Compare commits
372 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -30,6 +30,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/
|
||||||
|
app/app.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,4 +1,4 @@
|
|||||||
#Moonlight
|
# Moonlight
|
||||||
|
|
||||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
@@ -8,37 +8,37 @@ whether in your own home or over the internet.
|
|||||||
|
|
||||||
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
[Moonlight-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-android/wiki) for more detailed information or a troubleshooting guide.
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
##Features
|
## Features
|
||||||
|
|
||||||
* 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 +46,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)
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -4,72 +4,65 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 23
|
compileSdkVersion 26
|
||||||
buildToolsVersion "23.0.2"
|
buildToolsVersion '26.0.1'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 23
|
targetSdkVersion 26
|
||||||
|
|
||||||
versionName "4.0.2"
|
versionName "5.2"
|
||||||
versionCode = 79
|
versionCode = 131
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
applicationId "com.limelight.root"
|
applicationId "com.limelight.root"
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
|
}
|
||||||
|
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
applicationId "com.limelight"
|
applicationId "com.limelight"
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
|
}
|
||||||
|
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.57'
|
||||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.57'
|
||||||
|
implementation files('libs/jcodec-0.1.9-patched.jar')
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,75 +7,109 @@
|
|||||||
<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: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=".AppViewShortcutTrampoline"
|
||||||
android:logo="@drawable/atv_banner"
|
android:noHistory="true"
|
||||||
android:icon="@drawable/atv_banner"
|
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>
|
||||||
<activity
|
<activity
|
||||||
android:name=".Game"
|
android:name=".Game"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
android:theme="@style/StreamTheme"
|
android:noHistory="true"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
android:resizeableActivity="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 +119,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>
|
||||||
|
After Width: | Height: | Size: 58 KiB |
@@ -2,7 +2,6 @@ package com.limelight;
|
|||||||
|
|
||||||
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 java.util.UUID;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
@@ -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,6 @@ 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.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
@@ -43,6 +43,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
|
|||||||
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,6 +51,7 @@ 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;
|
||||||
@@ -77,6 +79,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
// Get the computer object
|
// Get the computer object
|
||||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||||
|
if (computer == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
@@ -89,15 +95,34 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the app grid with cached data (if possible).
|
||||||
|
// This must be done _before_ startComputerUpdates()
|
||||||
|
// so the initial serverinfo response can update the running
|
||||||
|
// icon.
|
||||||
|
populateAppGridWithCache();
|
||||||
|
|
||||||
// Start updates
|
// 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,13 +133,14 @@ 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;
|
||||||
@@ -139,6 +165,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.toString(),
|
||||||
|
getResources().getString(R.string.scut_not_paired));
|
||||||
|
|
||||||
|
// Display a toast to the user and quit the activity
|
||||||
|
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// App list is the same or empty
|
// App list is the same or empty
|
||||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||||
|
|
||||||
@@ -157,6 +201,7 @@ 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();
|
||||||
@@ -190,12 +235,9 @@ 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;
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
|
||||||
Configuration config = new Configuration(getResources().getConfiguration());
|
UiHelper.setLocale(this);
|
||||||
config.locale = new Locale(locale);
|
|
||||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_app_view);
|
setContentView(R.layout.activity_app_view);
|
||||||
|
|
||||||
@@ -203,11 +245,17 @@ 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);
|
|
||||||
|
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
||||||
|
TextView label = findViewById(R.id.appListText);
|
||||||
setTitle(labelText);
|
setTitle(labelText);
|
||||||
label.setText(labelText);
|
label.setText(labelText);
|
||||||
|
|
||||||
|
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||||
|
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
||||||
|
shortcutHelper.reportShortcutUsed(uuidString);
|
||||||
|
|
||||||
// Bind to the computer manager service
|
// Bind to the computer manager service
|
||||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||||
Service.BIND_AUTO_CREATE);
|
Service.BIND_AUTO_CREATE);
|
||||||
@@ -252,6 +300,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,30 +308,18 @@ 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));
|
||||||
}
|
}
|
||||||
@@ -358,19 +395,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 +437,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 +506,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 +519,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) {
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class AppViewShortcutTrampoline extends Activity {
|
||||||
|
private String uuidString;
|
||||||
|
|
||||||
|
private ComputerDetails computer;
|
||||||
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
|
|
||||||
|
public final static String UUID_EXTRA = "UUID";
|
||||||
|
|
||||||
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
|
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
|
|
||||||
|
// Wait in a separate thread to avoid stalling the UI
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Wait for the binder to be ready
|
||||||
|
localBinder.waitForReady();
|
||||||
|
|
||||||
|
// Now make the binder visible
|
||||||
|
managerBinder = localBinder;
|
||||||
|
|
||||||
|
// Get the computer object
|
||||||
|
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||||
|
|
||||||
|
// Force CMS to repoll this machine
|
||||||
|
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||||
|
// Don't care about other computers
|
||||||
|
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.state != ComputerDetails.State.UNKNOWN) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Stop showing the spinner
|
||||||
|
if (blockingLoadSpinner != null) {
|
||||||
|
blockingLoadSpinner.dismiss();
|
||||||
|
blockingLoadSpinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
|
// Close this activity
|
||||||
|
finish();
|
||||||
|
|
||||||
|
// Create a new activity stack for this launch
|
||||||
|
ArrayList<Intent> intentStack = new ArrayList<>();
|
||||||
|
Intent i;
|
||||||
|
|
||||||
|
// Add the PC view at the back (and clear the task)
|
||||||
|
i = new Intent(AppViewShortcutTrampoline.this, PcView.class);
|
||||||
|
i.setAction(Intent.ACTION_MAIN);
|
||||||
|
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intentStack.add(i);
|
||||||
|
|
||||||
|
// Take this intent's data and create an intent to start the app view
|
||||||
|
i = new Intent(getIntent());
|
||||||
|
i.setClass(AppViewShortcutTrampoline.this, AppView.class);
|
||||||
|
intentStack.add(i);
|
||||||
|
|
||||||
|
// If a game is running, we'll make the stream the top level activity
|
||||||
|
if (details.runningGameId != 0) {
|
||||||
|
intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this,
|
||||||
|
new NvApp("app", details.runningGameId), details, managerBinder));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now start the activities
|
||||||
|
startActivities(intentStack.toArray(new Intent[]{}));
|
||||||
|
}
|
||||||
|
else if (details.state == ComputerDetails.State.OFFLINE) {
|
||||||
|
// Computer offline - display an error dialog
|
||||||
|
Dialog.displayDialog(AppViewShortcutTrampoline.this,
|
||||||
|
getResources().getString(R.string.conn_error_title),
|
||||||
|
getResources().getString(R.string.error_pc_offline),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want any more callbacks from now on, so go ahead
|
||||||
|
// and unbind from the service
|
||||||
|
if (managerBinder != null) {
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||||
|
|
||||||
|
// Bind to the computer manager service
|
||||||
|
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||||
|
Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
|
||||||
|
getResources().getString(R.string.applist_connect_msg), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
|
||||||
|
if (blockingLoadSpinner != null) {
|
||||||
|
blockingLoadSpinner.dismiss();
|
||||||
|
blockingLoadSpinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,24 +4,29 @@ package com.limelight;
|
|||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.input.ControllerHandler;
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.binding.input.KeyboardTranslator;
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureManager;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
import com.limelight.binding.input.TouchContext;
|
import com.limelight.binding.input.TouchContext;
|
||||||
import com.limelight.binding.input.driver.UsbDriverService;
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.binding.input.evdev.EvdevHandler;
|
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
import com.limelight.binding.video.EnhancedDecoderRenderer;
|
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||||
|
import com.limelight.binding.video.CrashListener;
|
||||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.NvConnectionListener;
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
import com.limelight.nvstream.StreamConfiguration;
|
import com.limelight.nvstream.StreamConfiguration;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.input.KeyboardPacket;
|
import com.limelight.nvstream.input.KeyboardPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
|
import com.limelight.ui.StreamView;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -30,12 +35,13 @@ 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.content.SharedPreferences;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
@@ -45,19 +51,16 @@ import android.view.InputDevice;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.SurfaceView;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnGenericMotionListener;
|
import android.view.View.OnGenericMotionListener;
|
||||||
import android.view.View.OnSystemUiVisibilityChangeListener;
|
import android.view.View.OnSystemUiVisibilityChangeListener;
|
||||||
import android.view.View.OnTouchListener;
|
import android.view.View.OnTouchListener;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
|
|
||||||
public class Game extends Activity implements SurfaceHolder.Callback,
|
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||||
@@ -71,35 +74,35 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private final TouchContext[] touchContextMap = new TouchContext[2];
|
private final TouchContext[] touchContextMap = new TouchContext[2];
|
||||||
private long threeFingerDownTime = 0;
|
private long threeFingerDownTime = 0;
|
||||||
|
|
||||||
private static final double REFERENCE_HORIZ_RES = 1280;
|
private static final int REFERENCE_HORIZ_RES = 1280;
|
||||||
private static final double REFERENCE_VERT_RES = 720;
|
private static final int REFERENCE_VERT_RES = 720;
|
||||||
|
|
||||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||||
|
|
||||||
private ControllerHandler controllerHandler;
|
private ControllerHandler controllerHandler;
|
||||||
private KeyboardTranslator keybTranslator;
|
private VirtualController virtualController;
|
||||||
|
|
||||||
private PreferenceConfiguration prefConfig;
|
private PreferenceConfiguration prefConfig;
|
||||||
private final Point screenSize = new Point(0, 0);
|
private SharedPreferences tombstonePrefs;
|
||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
private SpinnerDialog spinner;
|
private SpinnerDialog spinner;
|
||||||
private boolean displayedFailureDialog = false;
|
private boolean displayedFailureDialog = false;
|
||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
private boolean deferredSurfaceResize = false;
|
|
||||||
|
|
||||||
private EvdevHandler evdevHandler;
|
private InputCaptureProvider inputCaptureProvider;
|
||||||
private int modifierFlags = 0;
|
private int modifierFlags = 0;
|
||||||
private boolean grabbedInput = true;
|
private boolean grabbedInput = true;
|
||||||
private boolean grabComboDown = false;
|
private boolean grabComboDown = false;
|
||||||
|
private StreamView streamView;
|
||||||
|
|
||||||
private EnhancedDecoderRenderer decoderRenderer;
|
private ShortcutHelper shortcutHelper;
|
||||||
|
|
||||||
|
private MediaCodecDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
private WifiManager.WifiLock wifiLock;
|
private WifiManager.WifiLock wifiLock;
|
||||||
|
|
||||||
private int drFlags = 0;
|
|
||||||
|
|
||||||
private boolean connectedToUsbDriverService = false;
|
private boolean connectedToUsbDriverService = false;
|
||||||
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
|
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
|
||||||
@Override
|
@Override
|
||||||
@@ -120,17 +123,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
public static final String EXTRA_APP_ID = "AppId";
|
public static final String EXTRA_APP_ID = "AppId";
|
||||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
|
public static final String EXTRA_PC_UUID = "UUID";
|
||||||
|
public static final String EXTRA_PC_NAME = "PcName";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
|
||||||
Configuration config = new Configuration(getResources().getConfiguration());
|
UiHelper.setLocale(this);
|
||||||
config.locale = new Locale(locale);
|
|
||||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't want a title bar
|
// We don't want a title bar
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
@@ -166,24 +168,33 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Read the stream preferences
|
// Read the stream preferences
|
||||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
|
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
|
||||||
if (prefConfig.stretchVideo) {
|
|
||||||
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
|
||||||
display.getSize(screenSize);
|
|
||||||
|
|
||||||
// Listen for events on the game surface
|
// Listen for events on the game surface
|
||||||
SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView);
|
streamView = findViewById(R.id.surfaceView);
|
||||||
sv.setOnGenericMotionListener(this);
|
streamView.setOnGenericMotionListener(this);
|
||||||
sv.setOnTouchListener(this);
|
streamView.setOnTouchListener(this);
|
||||||
|
|
||||||
|
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// The view must be focusable for pointer capture to work.
|
||||||
|
streamView.setFocusable(true);
|
||||||
|
streamView.setDefaultFocusHighlightEnabled(false);
|
||||||
|
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
|
||||||
|
return handleMotionEvent(motionEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Warn the user if they're on a metered connection
|
// Warn the user if they're on a metered connection
|
||||||
checkDataConnection();
|
checkDataConnection();
|
||||||
|
|
||||||
// Make sure Wi-Fi is fully powered up
|
// Make sure Wi-Fi is fully powered up
|
||||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||||
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
||||||
wifiLock.setReferenceCounted(false);
|
wifiLock.setReferenceCounted(false);
|
||||||
wifiLock.acquire();
|
wifiLock.acquire();
|
||||||
@@ -193,16 +204,35 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||||
|
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||||
|
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||||
|
|
||||||
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||||
|
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
|
||||||
|
shortcutHelper.reportShortcutUsed(uuid);
|
||||||
|
|
||||||
// Initialize the MediaCodec helper before creating the decoder
|
// Initialize the MediaCodec helper before creating the decoder
|
||||||
MediaCodecHelper.initializeWithContext(this);
|
MediaCodecHelper.initializeWithContext(this);
|
||||||
|
|
||||||
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat);
|
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat,
|
||||||
|
prefConfig.bitrate,
|
||||||
|
prefConfig.batterySaver,
|
||||||
|
new CrashListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyCrash(Exception e) {
|
||||||
|
// The MediaCodec instance is going down due to a crash
|
||||||
|
// let's tell the user something when they open the app again
|
||||||
|
|
||||||
|
// We must use commit because the app will crash when we return from this function
|
||||||
|
tombstonePrefs.edit().putInt("CrashCount", tombstonePrefs.getInt("CrashCount", 0) + 1).commit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tombstonePrefs.getInt("CrashCount", 0));
|
||||||
|
|
||||||
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
|
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
|
||||||
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
||||||
@@ -227,61 +257,44 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
.setApp(new NvApp(appName, appId))
|
.setApp(new NvApp(appName, appId))
|
||||||
.setBitrate(prefConfig.bitrate * 1000)
|
.setBitrate(prefConfig.bitrate * 1000)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
|
||||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
|
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
.setMaxPacketSize(remote ? 1024 : 1292)
|
.setMaxPacketSize(remote ? 1024 : 1292)
|
||||||
.setRemote(remote)
|
.setRemote(remote)
|
||||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||||
.setAudioConfiguration(prefConfig.enable51Surround ?
|
.setAudioConfiguration(prefConfig.enable51Surround ?
|
||||||
StreamConfiguration.AUDIO_CONFIGURATION_5_1 :
|
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
|
||||||
StreamConfiguration.AUDIO_CONFIGURATION_STEREO)
|
MoonBridge.AUDIO_CONFIGURATION_STEREO)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this));
|
||||||
keybTranslator = new KeyboardTranslator(conn);
|
controllerHandler = new ControllerHandler(this, conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
|
||||||
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
|
|
||||||
|
|
||||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
inputManager.registerInputDeviceListener(controllerHandler, null);
|
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||||
|
|
||||||
boolean aspectRatioMatch = false;
|
// Set to the optimal mode for streaming
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
prepareDisplayForRendering();
|
||||||
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
|
|
||||||
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
|
|
||||||
// setFixedSize so we can handle 4K properly. The only known devices that have
|
|
||||||
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
|
||||||
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
|
||||||
// 4K devices run 6.0 at the moment.
|
|
||||||
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
|
||||||
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
|
||||||
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
|
||||||
LimeLog.info("Stream has compatible aspect ratio with output display");
|
|
||||||
aspectRatioMatch = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SurfaceHolder sh = sv.getHolder();
|
|
||||||
if (prefConfig.stretchVideo || aspectRatioMatch) {
|
|
||||||
// Set the surface to the size of the video
|
|
||||||
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
deferredSurfaceResize = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize touch contexts
|
// Initialize touch contexts
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
touchContextMap[i] = new TouchContext(conn, i,
|
touchContextMap[i] = new TouchContext(conn, i,
|
||||||
(REFERENCE_HORIZ_RES / (double)screenSize.x),
|
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
||||||
(REFERENCE_VERT_RES / (double)screenSize.y));
|
streamView);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LimelightBuildProps.ROOT_BUILD) {
|
// Use sustained performance mode on N+ to ensure consistent
|
||||||
// Start watching for raw input
|
// CPU availability
|
||||||
evdevHandler = new EvdevHandler(this, this);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
evdevHandler.start();
|
getWindow().setSustainedPerformanceMode(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefConfig.onscreenController) {
|
||||||
|
// create virtual onscreen controller
|
||||||
|
virtualController = new VirtualController(conn,
|
||||||
|
(FrameLayout)streamView.getParent(),
|
||||||
|
this);
|
||||||
|
virtualController.refreshLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefConfig.usbDriver) {
|
if (prefConfig.usbDriver) {
|
||||||
@@ -291,22 +304,110 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The connection will be started when the surface gets created
|
// The connection will be started when the surface gets created
|
||||||
sh.addCallback(this);
|
streamView.getHolder().addCallback(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight)
|
@Override
|
||||||
{
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
// Get the visible width of the activity
|
super.onWindowFocusChanged(hasFocus);
|
||||||
double visibleWidth = getWindow().getDecorView().getWidth();
|
|
||||||
|
|
||||||
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// Capture is lost when focus is lost, so it must be requested again
|
||||||
|
// when focus is regained.
|
||||||
|
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
|
||||||
|
// Recapture the pointer if focus was regained
|
||||||
|
streamView.requestPointerCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the new size of the SurfaceView
|
private void prepareDisplayForRendering() {
|
||||||
lp.width = (int) visibleWidth;
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||||
|
|
||||||
// Apply the size change
|
// On M, we can explicitly set the optimal display mode
|
||||||
sv.setLayoutParams(lp);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Display.Mode bestMode = display.getMode();
|
||||||
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
|
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
|
||||||
|
candidate.getRefreshRate() < 63;
|
||||||
|
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
||||||
|
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
||||||
|
candidate.getPhysicalWidth() <= 4096;
|
||||||
|
|
||||||
|
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
|
||||||
|
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
|
||||||
|
|
||||||
|
// On non-4K streams, we force the resolution to never change
|
||||||
|
if (prefConfig.width < 3840) {
|
||||||
|
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
|
||||||
|
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the refresh rate doesn't regress
|
||||||
|
if (!refreshRateOk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the resolution doesn't regress
|
||||||
|
if (!resolutionOk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bestMode = candidate;
|
||||||
|
}
|
||||||
|
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||||
|
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||||
|
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||||
|
}
|
||||||
|
// On L, we can at least tell the OS that we want 60 Hz
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
float bestRefreshRate = display.getRefreshRate();
|
||||||
|
for (float candidate : display.getSupportedRefreshRates()) {
|
||||||
|
if (candidate > bestRefreshRate && candidate < 63) {
|
||||||
|
LimeLog.info("Examining refresh rate: "+candidate);
|
||||||
|
bestRefreshRate = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
||||||
|
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the display mode change
|
||||||
|
getWindow().setAttributes(windowLayoutParams);
|
||||||
|
|
||||||
|
// From 4.4 to 5.1 we can't ask for a 4K display mode, so we'll
|
||||||
|
// need to hint the OS to provide one.
|
||||||
|
boolean aspectRatioMatch = false;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
|
||||||
|
Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
|
||||||
|
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
|
||||||
|
// setFixedSize so we can handle 4K properly. The only known devices that have
|
||||||
|
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
||||||
|
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
||||||
|
// 4K devices run 6.0 at the moment.
|
||||||
|
Point screenSize = new Point(0, 0);
|
||||||
|
display.getSize(screenSize);
|
||||||
|
|
||||||
|
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
||||||
|
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
||||||
|
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
||||||
|
LimeLog.info("Stream has compatible aspect ratio with output display");
|
||||||
|
aspectRatioMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefConfig.stretchVideo || aspectRatioMatch) {
|
||||||
|
// Set the surface to the size of the video
|
||||||
|
streamView.getHolder().setFixedSize(prefConfig.width, prefConfig.height);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Set the surface to scale based on the aspect ratio of the stream
|
||||||
|
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkDataConnection()
|
private void checkDataConnection()
|
||||||
@@ -348,14 +449,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onDestroy() {
|
||||||
super.onStop();
|
super.onDestroy();
|
||||||
|
|
||||||
SpinnerDialog.closeDialogs(this);
|
if (controllerHandler != null) {
|
||||||
Dialog.closeDialogs();
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
|
inputManager.unregisterInputDeviceListener(controllerHandler);
|
||||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
}
|
||||||
inputManager.unregisterInputDeviceListener(controllerHandler);
|
|
||||||
|
|
||||||
wifiLock.release();
|
wifiLock.release();
|
||||||
|
|
||||||
@@ -364,36 +464,54 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
unbindService(usbDriverServiceConnection);
|
unbindService(usbDriverServiceConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoDecoderRenderer.VideoFormat videoFormat = conn.getActiveVideoFormat();
|
// Destroy the capture provider
|
||||||
|
inputCaptureProvider.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
displayedFailureDialog = true;
|
@Override
|
||||||
stopConnection();
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
SpinnerDialog.closeDialogs(this);
|
||||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
Dialog.closeDialogs();
|
||||||
String message = null;
|
|
||||||
if (averageEndToEndLat > 0) {
|
if (conn != null) {
|
||||||
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
||||||
if (averageDecoderLat > 0) {
|
|
||||||
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
displayedFailureDialog = true;
|
||||||
|
stopConnection();
|
||||||
|
|
||||||
|
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||||
|
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||||
|
String message = null;
|
||||||
|
if (averageEndToEndLat > 0) {
|
||||||
|
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
||||||
|
if (averageDecoderLat > 0) {
|
||||||
|
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else if (averageDecoderLat > 0) {
|
||||||
else if (averageDecoderLat > 0) {
|
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
||||||
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the video codec to the post-stream toast
|
|
||||||
if (message != null && videoFormat != VideoDecoderRenderer.VideoFormat.Unknown) {
|
|
||||||
if (videoFormat == VideoDecoderRenderer.VideoFormat.H265) {
|
|
||||||
message += " [H.265]";
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
message += " [H.264]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message != null) {
|
// Add the video codec to the post-stream toast
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
if (message != null) {
|
||||||
|
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
|
||||||
|
message += " [H.265]";
|
||||||
|
}
|
||||||
|
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
|
||||||
|
message += " [H.264]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the tombstone count
|
||||||
|
if (tombstonePrefs.getInt("CrashCount", 0) != 0) {
|
||||||
|
tombstonePrefs.edit().putInt("CrashCount", 0).apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
@@ -402,13 +520,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private final Runnable toggleGrab = new Runnable() {
|
private final Runnable toggleGrab = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (evdevHandler != null) {
|
if (grabbedInput) {
|
||||||
if (grabbedInput) {
|
inputCaptureProvider.disableCapture();
|
||||||
evdevHandler.ungrabAll();
|
}
|
||||||
}
|
else {
|
||||||
else {
|
inputCaptureProvider.enableCapture();
|
||||||
evdevHandler.regrabAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grabbedInput = !grabbedInput;
|
grabbedInput = !grabbedInput;
|
||||||
@@ -501,11 +617,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try the controller handler first
|
boolean handled = false;
|
||||||
boolean handled = controllerHandler.handleButtonDown(event);
|
|
||||||
|
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
||||||
|
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||||
|
if (detectedGamepad || (event.getDevice() == null ||
|
||||||
|
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
||||||
|
)) {
|
||||||
|
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
|
||||||
|
// Otherwise, controller handler will eat keyboard d-pad events.
|
||||||
|
handled = controllerHandler.handleButtonDown(event);
|
||||||
|
}
|
||||||
|
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
// Try the keyboard handler
|
// Try the keyboard handler
|
||||||
short translated = keybTranslator.translate(event.getKeyCode());
|
short translated = KeyboardTranslator.translate(event.getKeyCode());
|
||||||
if (translated == 0) {
|
if (translated == 0) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
}
|
}
|
||||||
@@ -515,18 +641,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eat repeat down events
|
|
||||||
if (event.getRepeatCount() > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass through keyboard input if we're not grabbing
|
// Pass through keyboard input if we're not grabbing
|
||||||
if (!grabbedInput) {
|
if (!grabbedInput) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
keybTranslator.sendKeyDown(translated,
|
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState());
|
||||||
getModifierState(event));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -539,11 +659,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try the controller handler first
|
boolean handled = false;
|
||||||
boolean handled = controllerHandler.handleButtonUp(event);
|
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
||||||
|
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||||
|
if (detectedGamepad || (event.getDevice() == null ||
|
||||||
|
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
||||||
|
)) {
|
||||||
|
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
|
||||||
|
// Otherwise, controller handler will eat keyboard d-pad events.
|
||||||
|
handled = controllerHandler.handleButtonUp(event);
|
||||||
|
}
|
||||||
|
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
// Try the keyboard handler
|
// Try the keyboard handler
|
||||||
short translated = keybTranslator.translate(event.getKeyCode());
|
short translated = KeyboardTranslator.translate(event.getKeyCode());
|
||||||
if (translated == 0) {
|
if (translated == 0) {
|
||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
}
|
}
|
||||||
@@ -557,8 +686,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
keybTranslator.sendKeyUp(translated,
|
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, getModifierState(event));
|
||||||
getModifierState(event));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -593,13 +721,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
|
||||||
|
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
|
||||||
{
|
{
|
||||||
// This case is for mice
|
// This case is for mice
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE)
|
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
|
(event.getPointerCount() >= 1 &&
|
||||||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
|
||||||
{
|
{
|
||||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||||
|
|
||||||
|
// Ignore mouse input if we're not capturing from our input source
|
||||||
|
if (!inputCaptureProvider.isCapturingActive()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
||||||
// Send the vertical scroll packet
|
// Send the vertical scroll packet
|
||||||
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||||
@@ -633,19 +769,38 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First process the history
|
// Get relative axis values if we can
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
||||||
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
// Send the deltas straight from the motion event
|
||||||
}
|
conn.sendMouseMove((short) inputCaptureProvider.getRelativeAxisX(event),
|
||||||
|
(short) inputCaptureProvider.getRelativeAxisY(event));
|
||||||
|
|
||||||
// Now process the current values
|
// We have to also update the position Android thinks the cursor is at
|
||||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
// in order to avoid jumping when we stop moving or click.
|
||||||
|
lastMouseX = (int)event.getX();
|
||||||
|
lastMouseY = (int)event.getY();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// First process the history
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process the current values
|
||||||
|
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||||
|
}
|
||||||
|
|
||||||
lastButtonState = event.getButtonState();
|
lastButtonState = event.getButtonState();
|
||||||
}
|
}
|
||||||
// This case is for touch-based input devices
|
// This case is for touch-based input devices
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (virtualController != null &&
|
||||||
|
virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) {
|
||||||
|
// Ignore presses when the virtual controller is in configuration mode
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int actionIndex = event.getActionIndex();
|
int actionIndex = event.getActionIndex();
|
||||||
|
|
||||||
int eventX = (int)event.getX(actionIndex);
|
int eventX = (int)event.getX(actionIndex);
|
||||||
@@ -756,8 +911,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Scale the deltas if the device resolution is different
|
// Scale the deltas if the device resolution is different
|
||||||
// than the stream resolution
|
// than the stream resolution
|
||||||
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)screenSize.x));
|
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)streamView.getWidth()));
|
||||||
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)screenSize.y));
|
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)streamView.getHeight()));
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
}
|
}
|
||||||
@@ -779,14 +934,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stageStarting(Stage stage) {
|
public void stageStarting(String stage) {
|
||||||
if (spinner != null) {
|
if (spinner != null) {
|
||||||
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage.getName());
|
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stageComplete(Stage stage) {
|
public void stageComplete(String stage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopConnection() {
|
private void stopConnection() {
|
||||||
@@ -794,36 +949,47 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
connecting = connected = false;
|
connecting = connected = false;
|
||||||
conn.stop();
|
conn.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the Evdev reader to allow use of captured input devices
|
|
||||||
if (evdevHandler != null) {
|
|
||||||
evdevHandler.stop();
|
|
||||||
evdevHandler = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stageFailed(Stage stage) {
|
public void stageFailed(String stage, long errorCode) {
|
||||||
if (spinner != null) {
|
if (spinner != null) {
|
||||||
spinner.dismiss();
|
spinner.dismiss();
|
||||||
spinner = null;
|
spinner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable cursor visibility again
|
||||||
|
inputCaptureProvider.disableCapture();
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
stopConnection();
|
LimeLog.severe(stage+" failed: "+errorCode);
|
||||||
|
|
||||||
|
// If video initialization failed and the surface is still valid, display extra information for the user
|
||||||
|
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||||
getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true);
|
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionTerminated(Exception e) {
|
public void connectionTerminated(long errorCode) {
|
||||||
|
// Enable cursor visibility again
|
||||||
|
inputCaptureProvider.disableCapture();
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
e.printStackTrace();
|
LimeLog.severe("Connection terminated: "+errorCode);
|
||||||
|
|
||||||
stopConnection();
|
stopConnection();
|
||||||
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
||||||
getResources().getString(R.string.conn_terminated_msg), true);
|
getResources().getString(R.string.conn_terminated_msg), true);
|
||||||
}
|
}
|
||||||
@@ -839,6 +1005,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
connecting = false;
|
connecting = false;
|
||||||
connected = true;
|
connected = true;
|
||||||
|
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Hide the mouse cursor now. Doing it before
|
||||||
|
// dismissing the spinner seems to be undone
|
||||||
|
// when the spinner gets displayed.
|
||||||
|
inputCaptureProvider.enableCapture();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
hideSystemUi(1000);
|
hideSystemUi(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,20 +1049,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (!connected && !connecting) {
|
if (!connected && !connecting) {
|
||||||
connecting = true;
|
connecting = true;
|
||||||
|
|
||||||
// Resize the surface to match the aspect ratio of the video
|
decoderRenderer.setRenderTarget(holder);
|
||||||
// This must be done after the surface is created.
|
conn.start(PlatformBinding.getAudioRenderer(), decoderRenderer, Game.this);
|
||||||
if (deferredSurfaceResize) {
|
|
||||||
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
|
||||||
prefConfig.width, prefConfig.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
|
||||||
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
|
// Let the decoder know immediately that the surface is gone
|
||||||
|
decoderRenderer.prepareForStop();
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
stopConnection();
|
stopConnection();
|
||||||
}
|
}
|
||||||
@@ -932,17 +1104,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void keyboardEvent(boolean buttonDown, short keyCode) {
|
public void keyboardEvent(boolean buttonDown, short keyCode) {
|
||||||
short keyMap = keybTranslator.translate(keyCode);
|
short keyMap = KeyboardTranslator.translate(keyCode);
|
||||||
if (keyMap != 0) {
|
if (keyMap != 0) {
|
||||||
if (handleSpecialKeys(keyMap, buttonDown)) {
|
if (handleSpecialKeys(keyMap, buttonDown)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buttonDown) {
|
if (buttonDown) {
|
||||||
keybTranslator.sendKeyDown(keyMap, getModifierState());
|
conn.sendKeyboardInput(keyMap, KeyboardPacket.KEY_DOWN, getModifierState());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
keybTranslator.sendKeyUp(keyMap, getModifierState());
|
conn.sendKeyboardInput(keyMap, KeyboardPacket.KEY_UP, getModifierState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -23,7 +21,9 @@ 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;
|
||||||
@@ -31,6 +31,7 @@ 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.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
@@ -52,8 +53,9 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
|
|||||||
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;
|
||||||
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 =
|
||||||
@@ -107,8 +109,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 +126,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 +151,9 @@ 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;
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
|
||||||
Configuration config = new Configuration(getResources().getConfiguration());
|
UiHelper.setLocale(this);
|
||||||
config.locale = new Locale(locale);
|
|
||||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind to the computer manager service
|
// Bind to the computer manager service
|
||||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||||
@@ -158,14 +164,38 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
||||||
|
|
||||||
initializeViews();
|
initializeViews();
|
||||||
|
|
||||||
|
SharedPreferences prefs = getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
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(this);
|
||||||
|
Dialog.displayDialog(this,
|
||||||
|
getResources().getString(R.string.title_decoding_reset),
|
||||||
|
getResources().getString(R.string.message_decoding_reset),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Dialog.displayDialog(this,
|
||||||
|
getResources().getString(R.string.title_decoding_error),
|
||||||
|
getResources().getString(R.string.message_decoding_error),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +245,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
hasResumed = true;
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +253,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,10 +301,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@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) {
|
||||||
@@ -302,19 +331,7 @@ 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(),
|
PlatformBinding.getDeviceName(),
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
@@ -330,17 +347,24 @@ 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.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
||||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
if (pairState == PairingManager.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 == PairingManager.PairState.FAILED) {
|
||||||
message = getResources().getString(R.string.pair_fail);
|
message = getResources().getString(R.string.pair_fail);
|
||||||
}
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) {
|
||||||
|
message = getResources().getString(R.string.pair_already_in_progress);
|
||||||
|
}
|
||||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||||
// 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;
|
||||||
|
|
||||||
|
// 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
|
||||||
@@ -368,14 +392,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();
|
||||||
}
|
}
|
||||||
@@ -431,19 +456,7 @@ 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(),
|
PlatformBinding.getDeviceName(),
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
@@ -560,6 +573,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.toString(),
|
||||||
|
getResources().getString(R.string.scut_deleted_pc));
|
||||||
|
|
||||||
pcGridAdapter.removeComputer(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
pcGridAdapter.notifyDataSetChanged();
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
@@ -586,6 +603,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a launcher shortcut for this PC
|
||||||
|
if (details.pairState == PairState.PAIRED) {
|
||||||
|
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false);
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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 {}
|
|
||||||
@@ -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() {
|
||||||
|
// Immediately drop all pending data
|
||||||
|
track.pause();
|
||||||
|
track.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
|
import android.view.InputEvent;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.input.driver.UsbDriverListener;
|
import com.limelight.binding.input.driver.UsbDriverListener;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
import com.limelight.utils.Vector2d;
|
import com.limelight.utils.Vector2d;
|
||||||
|
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
|
public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
|
||||||
|
|
||||||
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
||||||
|
|
||||||
private static final int START_DOWN_TIME_KEYB_MS = 750;
|
private static final int START_DOWN_TIME_MOUSE_MODE_MS = 750;
|
||||||
|
|
||||||
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
||||||
|
|
||||||
@@ -34,6 +41,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
|
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
|
||||||
|
|
||||||
private final NvConnection conn;
|
private final NvConnection conn;
|
||||||
|
private final Context activityContext;
|
||||||
private final double stickDeadzone;
|
private final double stickDeadzone;
|
||||||
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
||||||
private final GameGestures gestures;
|
private final GameGestures gestures;
|
||||||
@@ -42,7 +50,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private final boolean multiControllerEnabled;
|
private final boolean multiControllerEnabled;
|
||||||
private short currentControllers;
|
private short currentControllers;
|
||||||
|
|
||||||
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
||||||
|
this.activityContext = activityContext;
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.gestures = gestures;
|
this.gestures = gestures;
|
||||||
this.multiControllerEnabled = multiControllerEnabled;
|
this.multiControllerEnabled = multiControllerEnabled;
|
||||||
@@ -54,6 +63,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
int[] ids = InputDevice.getDeviceIds();
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
InputDevice dev = InputDevice.getDevice(id);
|
InputDevice dev = InputDevice.getDevice(id);
|
||||||
|
if (dev == null) {
|
||||||
|
// This device was removed during enumeration
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
||||||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
||||||
// This looks like a gamepad, but we'll check X and Y to be sure
|
// This looks like a gamepad, but we'll check X and Y to be sure
|
||||||
@@ -82,6 +95,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
defaultContext.controllerNumber = (short) 0;
|
defaultContext.controllerNumber = (short) 0;
|
||||||
|
defaultContext.assignedControllerNumber = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||||
@@ -122,7 +136,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private void releaseControllerNumber(GenericControllerContext context) {
|
private void releaseControllerNumber(GenericControllerContext context) {
|
||||||
// If this device sent data as a gamepad, zero the values before removing
|
// If this device sent data as a gamepad, zero the values before removing
|
||||||
if (context.assignedControllerNumber) {
|
if (context.assignedControllerNumber) {
|
||||||
conn.sendControllerInput(context.controllerNumber, (short) 0,
|
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
|
||||||
|
(short) 0,
|
||||||
(byte) 0, (byte) 0,
|
(byte) 0, (byte) 0,
|
||||||
(short) 0, (short) 0,
|
(short) 0, (short) 0,
|
||||||
(short) 0, (short) 0);
|
(short) 0, (short) 0);
|
||||||
@@ -146,8 +161,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
InputDeviceContext devContext = (InputDeviceContext) context;
|
InputDeviceContext devContext = (InputDeviceContext) context;
|
||||||
|
|
||||||
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
||||||
if (devContext.name != null && devContext.name.contains("gpio-keys")) {
|
if (devContext.name != null &&
|
||||||
// This is the back button on Shield portable consoles
|
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
|
||||||
|
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
|
||||||
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
}
|
}
|
||||||
@@ -212,6 +228,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
String devName = dev.getName();
|
String devName = dev.getName();
|
||||||
|
|
||||||
LimeLog.info("Creating controller context for device: "+devName);
|
LimeLog.info("Creating controller context for device: "+devName);
|
||||||
|
LimeLog.info(dev.toString());
|
||||||
|
|
||||||
context.name = devName;
|
context.name = devName;
|
||||||
context.id = dev.getId();
|
context.id = dev.getId();
|
||||||
@@ -229,6 +246,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
||||||
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
||||||
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
||||||
|
InputDevice.MotionRange throttleRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_THROTTLE);
|
||||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||||
{
|
{
|
||||||
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||||
@@ -241,6 +259,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
}
|
}
|
||||||
|
else if (brakeRange != null && throttleRange != null)
|
||||||
|
{
|
||||||
|
// Others use THROTTLE and BRAKE (like Xiaomi)
|
||||||
|
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
|
context.rightTriggerAxis = MotionEvent.AXIS_THROTTLE;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
@@ -320,12 +344,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore the back buttonn if a controller has both buttons
|
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
|
// The device name provided is just "Gamepad" which is pretty useless, so we
|
||||||
if (hasSelectKey[0] && hasSelectKey[1]) {
|
// use VID/PID instead
|
||||||
LimeLog.info("Ignoring back button because select is present");
|
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
|
||||||
context.ignoreBack = true;
|
context.backIsStart = true;
|
||||||
|
context.modeIsSelect = true;
|
||||||
|
context.triggerDeadzone = 0.30f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +388,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
|
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
|
||||||
// back presses on this device
|
// back presses on this device
|
||||||
else if (devName.equals("sec_touchscreen")) {
|
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey")) {
|
||||||
context.ignoreBack = true;
|
context.ignoreBack = true;
|
||||||
}
|
}
|
||||||
// The Serval has a couple of unknown buttons that are start and select. It also has
|
// The Serval has a couple of unknown buttons that are start and select. It also has
|
||||||
@@ -371,6 +397,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.isServal = true;
|
context.isServal = true;
|
||||||
context.ignoreBack = true;
|
context.ignoreBack = true;
|
||||||
}
|
}
|
||||||
|
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
|
||||||
|
// However, Microsoft released a firmware update with no change to VID/PID
|
||||||
|
// or device name that fixed the mappings for Android. Since there's
|
||||||
|
// no good way to detect this, we'll use the presence of GAS/BRAKE axes
|
||||||
|
// that were added in the latest firmware. If those are present, the only
|
||||||
|
// required fixup is ignoring the select button.
|
||||||
|
else if (devName.equals("Xbox Wireless Controller")) {
|
||||||
|
if (gasRange == null) {
|
||||||
|
context.isXboxBtController = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
|
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
|
||||||
@@ -379,31 +416,152 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private InputDeviceContext getContextForDevice(InputDevice dev) {
|
private InputDeviceContext getContextForEvent(InputEvent event) {
|
||||||
// Unknown devices use the default context
|
// Unknown devices use the default context
|
||||||
if (dev == null) {
|
if (event.getDeviceId() == 0) {
|
||||||
return defaultContext;
|
return defaultContext;
|
||||||
}
|
}
|
||||||
|
else if (event.getDevice() == null) {
|
||||||
|
// During device removal, sometimes we can get events after the
|
||||||
|
// input device has been destroyed. In this case we'll see a
|
||||||
|
// != 0 device ID but no device attached.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Return the existing context if it exists
|
// Return the existing context if it exists
|
||||||
InputDeviceContext context = inputDeviceContexts.get(dev.getId());
|
InputDeviceContext context = inputDeviceContexts.get(event.getDeviceId());
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise create a new context
|
// Otherwise create a new context
|
||||||
context = createInputDeviceContextForDevice(dev);
|
context = createInputDeviceContextForDevice(event.getDevice());
|
||||||
inputDeviceContexts.put(dev.getId(), context);
|
inputDeviceContexts.put(event.getDeviceId(), context);
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendControllerInputPacket(GenericControllerContext context) {
|
private byte maxByMagnitude(byte a, byte b) {
|
||||||
assignControllerNumberIfNeeded(context);
|
int absA = Math.abs(a);
|
||||||
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
int absB = Math.abs(b);
|
||||||
context.leftTrigger, context.rightTrigger,
|
if (absA > absB) {
|
||||||
context.leftStickX, context.leftStickY,
|
return a;
|
||||||
context.rightStickX, context.rightStickY);
|
}
|
||||||
|
else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private short maxByMagnitude(short a, short b) {
|
||||||
|
int absA = Math.abs(a);
|
||||||
|
int absB = Math.abs(b);
|
||||||
|
if (absA > absB) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private short getActiveControllerMask() {
|
||||||
|
if (multiControllerEnabled) {
|
||||||
|
return currentControllers;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Only Player 1 is active with multi-controller disabled
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendControllerInputPacket(GenericControllerContext originalContext) {
|
||||||
|
assignControllerNumberIfNeeded(originalContext);
|
||||||
|
|
||||||
|
// Take the context's controller number and fuse all inputs with the same number
|
||||||
|
short controllerNumber = originalContext.controllerNumber;
|
||||||
|
short inputMap = 0;
|
||||||
|
byte leftTrigger = 0;
|
||||||
|
byte rightTrigger = 0;
|
||||||
|
short leftStickX = 0;
|
||||||
|
short leftStickY = 0;
|
||||||
|
short rightStickX = 0;
|
||||||
|
short rightStickY = 0;
|
||||||
|
|
||||||
|
// In order to properly handle controllers that are split into multiple devices,
|
||||||
|
// we must aggregate all controllers with the same controller number into a single
|
||||||
|
// device before we send it.
|
||||||
|
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||||
|
GenericControllerContext context = inputDeviceContexts.valueAt(i);
|
||||||
|
if (context.assignedControllerNumber &&
|
||||||
|
context.controllerNumber == controllerNumber &&
|
||||||
|
context.mouseEmulationActive == originalContext.mouseEmulationActive) {
|
||||||
|
inputMap |= context.inputMap;
|
||||||
|
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
|
||||||
|
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
|
||||||
|
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
|
||||||
|
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
|
||||||
|
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
|
||||||
|
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < usbDeviceContexts.size(); i++) {
|
||||||
|
GenericControllerContext context = usbDeviceContexts.valueAt(i);
|
||||||
|
if (context.assignedControllerNumber &&
|
||||||
|
context.controllerNumber == controllerNumber &&
|
||||||
|
context.mouseEmulationActive == originalContext.mouseEmulationActive) {
|
||||||
|
inputMap |= context.inputMap;
|
||||||
|
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
|
||||||
|
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
|
||||||
|
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
|
||||||
|
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
|
||||||
|
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
|
||||||
|
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (defaultContext.controllerNumber == controllerNumber) {
|
||||||
|
inputMap |= defaultContext.inputMap;
|
||||||
|
leftTrigger |= maxByMagnitude(leftTrigger, defaultContext.leftTrigger);
|
||||||
|
rightTrigger |= maxByMagnitude(rightTrigger, defaultContext.rightTrigger);
|
||||||
|
leftStickX |= maxByMagnitude(leftStickX, defaultContext.leftStickX);
|
||||||
|
leftStickY |= maxByMagnitude(leftStickY, defaultContext.leftStickY);
|
||||||
|
rightStickX |= maxByMagnitude(rightStickX, defaultContext.rightStickX);
|
||||||
|
rightStickY |= maxByMagnitude(rightStickY, defaultContext.rightStickY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalContext.mouseEmulationActive) {
|
||||||
|
int changedMask = inputMap ^ originalContext.mouseEmulationLastInputMap;
|
||||||
|
|
||||||
|
boolean aDown = (inputMap & ControllerPacket.A_FLAG) != 0;
|
||||||
|
boolean bDown = (inputMap & ControllerPacket.B_FLAG) != 0;
|
||||||
|
|
||||||
|
originalContext.mouseEmulationLastInputMap = inputMap;
|
||||||
|
|
||||||
|
if ((changedMask & ControllerPacket.A_FLAG) != 0) {
|
||||||
|
if (aDown) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((changedMask & ControllerPacket.B_FLAG) != 0) {
|
||||||
|
if (bDown) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
|
||||||
|
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
|
||||||
|
inputMap,
|
||||||
|
leftTrigger, rightTrigger,
|
||||||
|
leftStickX, leftStickY,
|
||||||
|
rightStickX, rightStickY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
||||||
@@ -417,41 +575,38 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (context.isDualShock4) {
|
if (context.isDualShock4) {
|
||||||
switch (event.getKeyCode()) {
|
switch (event.getScanCode()) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
case 304:
|
||||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
|
case 305:
|
||||||
case KeyEvent.KEYCODE_BUTTON_Z:
|
return KeyEvent.KEYCODE_BUTTON_A;
|
||||||
return KeyEvent.KEYCODE_BUTTON_R1;
|
case 306:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_B;
|
||||||
case KeyEvent.KEYCODE_BUTTON_C:
|
case 307:
|
||||||
return KeyEvent.KEYCODE_BUTTON_B;
|
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||||
|
case 308:
|
||||||
case KeyEvent.KEYCODE_BUTTON_X:
|
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
case 309:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||||
case KeyEvent.KEYCODE_BUTTON_B:
|
/*
|
||||||
return KeyEvent.KEYCODE_BUTTON_A;
|
**** Using analog triggers instead ****
|
||||||
|
case 310:
|
||||||
case KeyEvent.KEYCODE_BUTTON_A:
|
return KeyEvent.KEYCODE_BUTTON_L2;
|
||||||
return KeyEvent.KEYCODE_BUTTON_X;
|
case 311:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_R2;
|
||||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
*/
|
||||||
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
case 312:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
case KeyEvent.KEYCODE_BUTTON_START:
|
case 313:
|
||||||
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
case 314:
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
case 315:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case 316:
|
||||||
return KeyEvent.KEYCODE_BUTTON_START;
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
default:
|
||||||
// These are duplicate trigger events
|
return 0;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
|
||||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If this is a Serval controller sending an unknown key code, it's probably
|
// If this is a Serval controller sending an unknown key code, it's probably
|
||||||
@@ -464,6 +619,35 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return KeyEvent.KEYCODE_BUTTON_START;
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (context.isXboxBtController) {
|
||||||
|
switch (event.getScanCode()) {
|
||||||
|
case 306:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
|
case 307:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||||
|
case 308:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||||
|
case 309:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||||
|
case 310:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
|
case 311:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
case 312:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||||
|
case 313:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||||
|
case 139:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
default:
|
||||||
|
// Other buttons are mapped correctly
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Xbox button is sent as MENU
|
||||||
|
if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
||||||
switch (event.getKeyCode()) {
|
switch (event.getKeyCode()) {
|
||||||
@@ -478,7 +662,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
else if (context.hatXAxis == -1 &&
|
else if (context.hatXAxis == -1 &&
|
||||||
context.hatYAxis == -1 &&
|
context.hatYAxis == -1 &&
|
||||||
context.isXboxController &&
|
/* FIXME: There's no good way to know for sure if xpad is bound
|
||||||
|
to this device, so we won't use the name to validate if these
|
||||||
|
scancodes should be mapped to DPAD
|
||||||
|
|
||||||
|
context.isXboxController &&
|
||||||
|
*/
|
||||||
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
||||||
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
|
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
|
||||||
// scan codes into proper key codes
|
// scan codes into proper key codes
|
||||||
@@ -568,9 +757,23 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
||||||
|
// Android sends an initial 0 value for trigger axes even if the trigger
|
||||||
|
// should be negative when idle. After the first touch, the axes will go back
|
||||||
|
// to normal behavior, so ignore triggersIdleNegative for each trigger until
|
||||||
|
// first touch.
|
||||||
|
if (lt != 0) {
|
||||||
|
context.leftTriggerUsed = true;
|
||||||
|
}
|
||||||
|
if (rt != 0) {
|
||||||
|
context.rightTriggerUsed = true;
|
||||||
|
}
|
||||||
if (context.triggersIdleNegative) {
|
if (context.triggersIdleNegative) {
|
||||||
lt = (lt + 1) / 2;
|
if (context.leftTriggerUsed) {
|
||||||
rt = (rt + 1) / 2;
|
lt = (lt + 1) / 2;
|
||||||
|
}
|
||||||
|
if (context.rightTriggerUsed) {
|
||||||
|
rt = (rt + 1) / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lt <= context.triggerDeadzone) {
|
if (lt <= context.triggerDeadzone) {
|
||||||
@@ -606,7 +809,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleMotionEvent(MotionEvent event) {
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
InputDeviceContext context = getContextForDevice(event.getDevice());
|
InputDeviceContext context = getContextForEvent(event);
|
||||||
|
if (context == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
|
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
|
||||||
|
|
||||||
// We purposefully ignore the historical values in the motion event as it makes
|
// We purposefully ignore the historical values in the motion event as it makes
|
||||||
@@ -637,8 +844,51 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private short scaleRawStickAxis(float stickValue) {
|
||||||
|
return (short)Math.pow(stickValue, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendEmulatedMouseEvent(short x, short y) {
|
||||||
|
Vector2d vector = new Vector2d();
|
||||||
|
vector.initialize(x, y);
|
||||||
|
vector.scalarMultiply(1 / 32766.0f);
|
||||||
|
vector.scalarMultiply(4);
|
||||||
|
if (vector.getMagnitude() > 0) {
|
||||||
|
// Move faster as the stick is pressed further from center
|
||||||
|
vector.scalarMultiply(Math.pow(vector.getMagnitude(), 2));
|
||||||
|
if (vector.getMagnitude() >= 1) {
|
||||||
|
conn.sendMouseMove((short)vector.getX(), (short)-vector.getY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleMouseEmulation(final GenericControllerContext context) {
|
||||||
|
if (context.mouseEmulationTimer != null) {
|
||||||
|
context.mouseEmulationTimer.cancel();
|
||||||
|
context.mouseEmulationTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.mouseEmulationActive = !context.mouseEmulationActive;
|
||||||
|
Toast.makeText(activityContext, "Mouse emulation is: " + (context.mouseEmulationActive ? "ON" : "OFF"), Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
if (context.mouseEmulationActive) {
|
||||||
|
context.mouseEmulationTimer = new Timer();
|
||||||
|
context.mouseEmulationTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Send mouse movement events from analog sticks
|
||||||
|
sendEmulatedMouseEvent(context.leftStickX, context.leftStickY);
|
||||||
|
sendEmulatedMouseEvent(context.rightStickX, context.rightStickY);
|
||||||
|
}
|
||||||
|
}, 50, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean handleButtonUp(KeyEvent event) {
|
public boolean handleButtonUp(KeyEvent event) {
|
||||||
InputDeviceContext context = getContextForDevice(event.getDevice());
|
InputDeviceContext context = getContextForEvent(event);
|
||||||
|
if (context == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int keyCode = handleRemapping(context, event);
|
int keyCode = handleRemapping(context, event);
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
@@ -663,10 +913,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_START:
|
case KeyEvent.KEYCODE_BUTTON_START:
|
||||||
case KeyEvent.KEYCODE_MENU:
|
case KeyEvent.KEYCODE_MENU:
|
||||||
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
|
// Sometimes we'll get a spurious key up event on controller disconnect.
|
||||||
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
|
// Make sure it's real by checking that the key is actually down before taking
|
||||||
// until we have a custom keyboard or some other fix
|
// any action.
|
||||||
//gestures.showKeyboard();
|
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||||
|
SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS) {
|
||||||
|
toggleMouseEmulation(context);
|
||||||
}
|
}
|
||||||
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||||
break;
|
break;
|
||||||
@@ -763,7 +1015,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleButtonDown(KeyEvent event) {
|
public boolean handleButtonDown(KeyEvent event) {
|
||||||
InputDeviceContext context = getContextForDevice(event.getDevice());
|
InputDeviceContext context = getContextForEvent(event);
|
||||||
|
if (context == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int keyCode = handleRemapping(context, event);
|
int keyCode = handleRemapping(context, event);
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
@@ -855,11 +1110,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a new input packet if this is the first instance of a button down event
|
|
||||||
// or anytime if we're emulating a button
|
// We don't need to send repeat key down events, but the platform
|
||||||
if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
|
// sends us events that claim to be repeats but they're from different
|
||||||
sendControllerInputPacket(context);
|
// devices, so we just send them all and deal with some duplicates.
|
||||||
}
|
sendControllerInputPacket(context);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,6 +1124,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
float rightStickX, float rightStickY,
|
float rightStickX, float rightStickY,
|
||||||
float leftTrigger, float rightTrigger) {
|
float leftTrigger, float rightTrigger) {
|
||||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Vector2d leftStickVector = populateCachedVector(leftStickX, leftStickY);
|
Vector2d leftStickVector = populateCachedVector(leftStickX, leftStickY);
|
||||||
|
|
||||||
@@ -933,6 +1191,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public short rightStickY = 0x0000;
|
public short rightStickY = 0x0000;
|
||||||
public short leftStickX = 0x0000;
|
public short leftStickX = 0x0000;
|
||||||
public short leftStickY = 0x0000;
|
public short leftStickY = 0x0000;
|
||||||
|
|
||||||
|
public boolean mouseEmulationActive;
|
||||||
|
public Timer mouseEmulationTimer;
|
||||||
|
public short mouseEmulationLastInputMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputDeviceContext extends GenericControllerContext {
|
class InputDeviceContext extends GenericControllerContext {
|
||||||
@@ -947,12 +1209,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public int leftTriggerAxis = -1;
|
public int leftTriggerAxis = -1;
|
||||||
public int rightTriggerAxis = -1;
|
public int rightTriggerAxis = -1;
|
||||||
public boolean triggersIdleNegative;
|
public boolean triggersIdleNegative;
|
||||||
|
public boolean leftTriggerUsed, rightTriggerUsed;
|
||||||
|
|
||||||
public int hatXAxis = -1;
|
public int hatXAxis = -1;
|
||||||
public int hatYAxis = -1;
|
public int hatYAxis = -1;
|
||||||
|
|
||||||
public boolean isDualShock4;
|
public boolean isDualShock4;
|
||||||
public boolean isXboxController;
|
public boolean isXboxController;
|
||||||
|
public boolean isXboxBtController;
|
||||||
public boolean isServal;
|
public boolean isServal;
|
||||||
public boolean backIsStart;
|
public boolean backIsStart;
|
||||||
public boolean modeIsSelect;
|
public boolean modeIsSelect;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -59,21 +56,12 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
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;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new translator for the specified connection
|
|
||||||
* @param conn the connection to which the translated codes are sent
|
|
||||||
*/
|
|
||||||
public KeyboardTranslator(NvConnection conn) {
|
|
||||||
super(conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates the given keycode and returns the GFE keycode
|
* 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
|
/* There seems to be no clean mapping between Android key codes
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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) {
|
||||||
|
return event.getX();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return event.getY();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,35 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
|
||||||
|
public class InputCaptureManager {
|
||||||
|
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||||
|
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using Android O+ native mouse capture");
|
||||||
|
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
||||||
|
}
|
||||||
|
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||||
|
return new ShieldCaptureProvider(activity);
|
||||||
|
}
|
||||||
|
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using Evdev mouse capture");
|
||||||
|
return new EvdevCaptureProvider(activity, rootListener);
|
||||||
|
}
|
||||||
|
else if (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,47 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
public abstract class AbstractController {
|
||||||
|
|
||||||
|
private final int deviceId;
|
||||||
|
|
||||||
|
private UsbDriverListener listener;
|
||||||
|
|
||||||
|
protected short buttonFlags;
|
||||||
|
protected float leftTrigger, rightTrigger;
|
||||||
|
protected float rightStickX, rightStickY;
|
||||||
|
protected float leftStickX, leftStickY;
|
||||||
|
|
||||||
|
public int getControllerId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setButtonFlag(int buttonFlag, int data) {
|
||||||
|
if (data != 0) {
|
||||||
|
buttonFlags |= buttonFlag;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buttonFlags &= ~buttonFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void reportInput() {
|
||||||
|
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
||||||
|
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean start();
|
||||||
|
public abstract void stop();
|
||||||
|
|
||||||
|
public AbstractController(int deviceId, UsbDriverListener listener) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void notifyDeviceRemoved() {
|
||||||
|
listener.deviceRemoved(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void notifyDeviceAdded() {
|
||||||
|
listener.deviceAdded(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
import android.hardware.usb.UsbEndpoint;
|
||||||
|
import android.hardware.usb.UsbInterface;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
public abstract class AbstractXboxController extends AbstractController {
|
||||||
|
protected final UsbDevice device;
|
||||||
|
protected final UsbDeviceConnection connection;
|
||||||
|
|
||||||
|
private Thread inputThread;
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
protected UsbEndpoint inEndpt, outEndpt;
|
||||||
|
|
||||||
|
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
|
super(deviceId, listener);
|
||||||
|
this.device = device;
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Thread createInputThread() {
|
||||||
|
return new Thread() {
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted() && !stopped) {
|
||||||
|
byte[] buffer = new byte[64];
|
||||||
|
|
||||||
|
int res;
|
||||||
|
|
||||||
|
//
|
||||||
|
// There's no way that I can tell to determine if a device has failed
|
||||||
|
// or if the timeout has simply expired. We'll check how long the transfer
|
||||||
|
// took to fail and assume the device failed if it happened before the timeout
|
||||||
|
// expired.
|
||||||
|
//
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Read the next input state packet
|
||||||
|
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
||||||
|
|
||||||
|
// If we get a zero length response, treat it as an error
|
||||||
|
if (res == 0) {
|
||||||
|
res = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
||||||
|
LimeLog.warning("Detected device I/O error");
|
||||||
|
AbstractXboxController.this.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (res == -1 && !isInterrupted() && !stopped);
|
||||||
|
|
||||||
|
if (res == -1 || stopped) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
|
||||||
|
// Report input if handleRead() returns true
|
||||||
|
reportInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean start() {
|
||||||
|
// Force claim all interfaces
|
||||||
|
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||||
|
UsbInterface iface = device.getInterface(i);
|
||||||
|
|
||||||
|
if (!connection.claimInterface(iface, true)) {
|
||||||
|
LimeLog.warning("Failed to claim interfaces");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the endpoints
|
||||||
|
UsbInterface iface = device.getInterface(0);
|
||||||
|
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
||||||
|
UsbEndpoint endpt = iface.getEndpoint(i);
|
||||||
|
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||||
|
if (inEndpt != null) {
|
||||||
|
LimeLog.warning("Found duplicate IN endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
inEndpt = endpt;
|
||||||
|
}
|
||||||
|
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||||
|
if (outEndpt != null) {
|
||||||
|
LimeLog.warning("Found duplicate OUT endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outEndpt = endpt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the required endpoints were present
|
||||||
|
if (inEndpt == null || outEndpt == null) {
|
||||||
|
LimeLog.warning("Missing required endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the init function
|
||||||
|
if (!doInit()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report that we're added _before_ starting the input thread
|
||||||
|
notifyDeviceAdded();
|
||||||
|
|
||||||
|
// Start listening for controller input
|
||||||
|
inputThread = createInputThread();
|
||||||
|
inputThread.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped = true;
|
||||||
|
|
||||||
|
// Stop the input thread
|
||||||
|
if (inputThread != null) {
|
||||||
|
inputThread.interrupt();
|
||||||
|
inputThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the USB connection
|
||||||
|
connection.close();
|
||||||
|
|
||||||
|
// Report the device removed
|
||||||
|
notifyDeviceRemoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract boolean handleRead(ByteBuffer buffer);
|
||||||
|
protected abstract boolean doInit();
|
||||||
|
}
|
||||||
@@ -10,7 +10,11 @@ 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.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@@ -24,10 +28,10 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
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) {
|
||||||
@@ -40,7 +44,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
@Override
|
@Override
|
||||||
public void deviceRemoved(int controllerId) {
|
public void deviceRemoved(int controllerId) {
|
||||||
// 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) {
|
for (AbstractController controller : controllers) {
|
||||||
if (controller.getControllerId() == controllerId) {
|
if (controller.getControllerId() == controllerId) {
|
||||||
controllers.remove(controller);
|
controllers.remove(controller);
|
||||||
break;
|
break;
|
||||||
@@ -68,14 +72,14 @@ 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);
|
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
// Continue the state machine
|
// Continue the state machine
|
||||||
handleUsbDeviceState(device);
|
handleUsbDeviceState(device);
|
||||||
}
|
}
|
||||||
// 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,7 +95,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Report all controllerMap that already exist
|
// Report all controllerMap that already exist
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
for (XboxOneController controller : controllers) {
|
for (AbstractController controller : controllers) {
|
||||||
listener.deviceAdded(controller.getControllerId());
|
listener.deviceAdded(controller.getControllerId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +104,7 @@ 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)) {
|
||||||
// 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
|
||||||
@@ -110,9 +114,26 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// 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,6 +144,34 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isRecognizedInputDevice(UsbDevice device) {
|
||||||
|
// On KitKat and later, we can determine if this VID and PID combo
|
||||||
|
// matches an existing input device and defer to the built-in controller
|
||||||
|
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
for (int id : InputDevice.getDeviceIds()) {
|
||||||
|
InputDevice inputDev = InputDevice.getDevice(id);
|
||||||
|
|
||||||
|
if (inputDev.getVendorId() == device.getVendorId() &&
|
||||||
|
inputDev.getProductId() == device.getProductId()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldClaimDevice(UsbDevice device) {
|
||||||
|
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||||
|
// if we know the kernel isn't already driving this device.
|
||||||
|
return XboxOneController.canClaimDevice(device) ||
|
||||||
|
(!isRecognizedInputDevice(device) && Xbox360Controller.canClaimDevice(device));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||||
@@ -135,7 +184,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)) {
|
||||||
// Start the process of claiming this device
|
// Start the process of claiming this device
|
||||||
handleUsbDeviceState(dev);
|
handleUsbDeviceState(dev);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class Xbox360Controller extends AbstractXboxController {
|
||||||
|
private static final int XB360_IFACE_SUBCLASS = 93;
|
||||||
|
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||||
|
|
||||||
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x044f, // Thrustmaster
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x046d, // Logitech
|
||||||
|
0x056e, // Elecom
|
||||||
|
0x06a3, // Saitek
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x07ff, // Mad Catz
|
||||||
|
0x0e6f, // Unknown
|
||||||
|
0x0f0d, // Hori
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,60 +3,31 @@ 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;
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // Unknown
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1532, // Razer Wildcat
|
||||||
|
0x24c6, // PowerA
|
||||||
|
};
|
||||||
|
|
||||||
// FIXME: odata_serial
|
// FIXME: odata_serial
|
||||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||||
|
|
||||||
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,103 +61,56 @@ 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
|
||||||
|
// it retransmits them forever.
|
||||||
|
if (buffer.get() == 0x30) {
|
||||||
|
ackModeReport(buffer.get());
|
||||||
|
buffer.position(buffer.position() + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer.position(buffer.position() + 2);
|
||||||
|
}
|
||||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||||
reportInput();
|
return true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startInputThread(final UsbEndpoint inEndpt) {
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
inputThread = new Thread() {
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
public void run() {
|
if (device.getVendorId() == supportedVid &&
|
||||||
while (!isInterrupted() && !stopped) {
|
device.getInterfaceCount() >= 1 &&
|
||||||
byte[] buffer = new byte[64];
|
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||||
int res;
|
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||||
|
return true;
|
||||||
//
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
inputThread.setName("Xbox One Controller - Input Thread");
|
|
||||||
inputThread.start();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean start() {
|
@Override
|
||||||
// Force claim all interfaces
|
protected boolean doInit() {
|
||||||
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
|
|
||||||
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
|
// Send the initialization packet
|
||||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
||||||
if (res != XB1_INIT_DATA.length) {
|
if (res != XB1_INIT_DATA.length) {
|
||||||
@@ -194,40 +118,6 @@ public class XboxOneController {
|
|||||||
return false;
|
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() {
|
|
||||||
if (stopped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return device.getVendorId() == MICROSOFT_VID &&
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
|
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
public class EvdevCaptureProvider extends InputCaptureProvider {
|
||||||
|
|
||||||
|
private final EvdevListener listener;
|
||||||
|
private final String libraryPath;
|
||||||
|
|
||||||
|
private boolean shutdown = false;
|
||||||
|
private InputStream evdevIn;
|
||||||
|
private OutputStream evdevOut;
|
||||||
|
private Process su;
|
||||||
|
private ServerSocket servSock;
|
||||||
|
private Socket evdevSock;
|
||||||
|
private Activity activity;
|
||||||
|
private boolean started = false;
|
||||||
|
|
||||||
|
private static final byte UNGRAB_REQUEST = 1;
|
||||||
|
private static final byte REGRAB_REQUEST = 2;
|
||||||
|
|
||||||
|
private final Thread handlerThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int deltaX = 0;
|
||||||
|
int deltaY = 0;
|
||||||
|
byte deltaScroll = 0;
|
||||||
|
|
||||||
|
// Bind a local listening socket for evdevreader to connect to
|
||||||
|
try {
|
||||||
|
servSock = new ServerSocket(0, 1);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String evdevReaderCmd = libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort();
|
||||||
|
|
||||||
|
// On Nougat and later, we'll need to pass the command directly to SU.
|
||||||
|
// Writing to SU's input stream after it has started doesn't seem to work anymore.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
// Launch evdev_reader directly via SU
|
||||||
|
try {
|
||||||
|
su = Runtime.getRuntime().exec("su -c "+evdevReaderCmd);
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportDeviceNotRooted();
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Launch a SU shell on Marshmallow and earlier
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("su");
|
||||||
|
builder.redirectErrorStream(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
su = builder.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportDeviceNotRooted();
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start evdevreader
|
||||||
|
DataOutputStream suOut = new DataOutputStream(su.getOutputStream());
|
||||||
|
try {
|
||||||
|
suOut.writeChars(evdevReaderCmd+"\n");
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportDeviceNotRooted();
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for evdevreader's connection
|
||||||
|
LimeLog.info("Waiting for EvdevReader connection to port "+servSock.getLocalPort());
|
||||||
|
try {
|
||||||
|
evdevSock = servSock.accept();
|
||||||
|
evdevIn = evdevSock.getInputStream();
|
||||||
|
evdevOut = evdevSock.getOutputStream();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LimeLog.info("EvdevReader connected from port "+evdevSock.getPort());
|
||||||
|
|
||||||
|
while (!isInterrupted() && !shutdown) {
|
||||||
|
EvdevEvent event;
|
||||||
|
try {
|
||||||
|
event = EvdevReader.read(evdevIn);
|
||||||
|
} catch (IOException e) {
|
||||||
|
event = null;
|
||||||
|
}
|
||||||
|
if (event == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case EvdevEvent.EV_SYN:
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
listener.mouseMove(deltaX, deltaY);
|
||||||
|
deltaX = deltaY = 0;
|
||||||
|
}
|
||||||
|
if (deltaScroll != 0) {
|
||||||
|
listener.mouseScroll(deltaScroll);
|
||||||
|
deltaScroll = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_REL:
|
||||||
|
switch (event.code) {
|
||||||
|
case EvdevEvent.REL_X:
|
||||||
|
deltaX = event.value;
|
||||||
|
break;
|
||||||
|
case EvdevEvent.REL_Y:
|
||||||
|
deltaY = event.value;
|
||||||
|
break;
|
||||||
|
case EvdevEvent.REL_WHEEL:
|
||||||
|
deltaScroll = (byte) event.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_KEY:
|
||||||
|
switch (event.code) {
|
||||||
|
case EvdevEvent.BTN_LEFT:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
case EvdevEvent.BTN_MIDDLE:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
case EvdevEvent.BTN_RIGHT:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.BTN_SIDE:
|
||||||
|
case EvdevEvent.BTN_EXTRA:
|
||||||
|
case EvdevEvent.BTN_FORWARD:
|
||||||
|
case EvdevEvent.BTN_BACK:
|
||||||
|
case EvdevEvent.BTN_TASK:
|
||||||
|
// Other unhandled mouse buttons
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// We got some unrecognized button. This means
|
||||||
|
// someone is trying to use the other device in this
|
||||||
|
// "combination" input device. We'll try to handle
|
||||||
|
// it via keyboard, but we're not going to disconnect
|
||||||
|
// if we can't
|
||||||
|
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
||||||
|
if (keyCode != 0) {
|
||||||
|
listener.keyboardEvent(event.value != 0, keyCode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_MSC:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
this.activity = activity;
|
||||||
|
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return LimelightBuildProps.ROOT_BUILD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportDeviceNotRooted() {
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(activity, "This device is not rooted - Mouse capture is unavailable", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runInNetworkSafeContextSynchronously(Runnable runnable) {
|
||||||
|
// This function is used to avoid Android's strict NetworkOnMainThreadException.
|
||||||
|
// For our usage, it is highly unlikely to cause problems since we only do
|
||||||
|
// write operations and only to localhost sockets.
|
||||||
|
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
|
||||||
|
Thread t = new Thread(runnable);
|
||||||
|
t.start();
|
||||||
|
try {
|
||||||
|
t.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// The main thread should never be interrupted
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Run the runnable directly
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
super.enableCapture();
|
||||||
|
if (!started) {
|
||||||
|
// Start the handler thread if it's our first time
|
||||||
|
// capturing
|
||||||
|
handlerThread.start();
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This may be called on the main thread
|
||||||
|
runInNetworkSafeContextSynchronously(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Send a request to regrab if we're already capturing
|
||||||
|
if (!shutdown && evdevOut != null) {
|
||||||
|
try {
|
||||||
|
evdevOut.write(REGRAB_REQUEST);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
super.disableCapture();
|
||||||
|
// This may be called on the main thread
|
||||||
|
runInNetworkSafeContextSynchronously(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (started && !shutdown && evdevOut != null) {
|
||||||
|
try {
|
||||||
|
evdevOut.write(UNGRAB_REQUEST);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
// We need to stop the process in this context otherwise
|
||||||
|
// we could get stuck waiting on output from the process
|
||||||
|
// in order to terminate it.
|
||||||
|
//
|
||||||
|
// This may be called on the main thread.
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown = true;
|
||||||
|
handlerThread.interrupt();
|
||||||
|
|
||||||
|
runInNetworkSafeContextSynchronously(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (servSock != null) {
|
||||||
|
try {
|
||||||
|
servSock.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evdevSock != null) {
|
||||||
|
try {
|
||||||
|
evdevSock.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evdevIn != null) {
|
||||||
|
try {
|
||||||
|
evdevIn.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evdevOut != null) {
|
||||||
|
try {
|
||||||
|
evdevOut.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (su != null) {
|
||||||
|
su.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
handlerThread.join();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,12 @@
|
|||||||
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;
|
||||||
|
|
||||||
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,349 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a analog stick on screen element. It is used to get 2-Axis user input.
|
||||||
|
*/
|
||||||
|
public class AnalogStick extends VirtualControllerElement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* outer radius size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_COMPLETE = 90;
|
||||||
|
/**
|
||||||
|
* analog stick size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
|
||||||
|
/**
|
||||||
|
* dead zone size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_DEADZONE = 90;
|
||||||
|
/**
|
||||||
|
* time frame for a double click
|
||||||
|
*/
|
||||||
|
public final static long timeoutDoubleClick = 350;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
|
||||||
|
*/
|
||||||
|
public final static long timeoutDeadzone = 150;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface to update registered observers.
|
||||||
|
*/
|
||||||
|
public interface AnalogStickListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
|
||||||
|
*
|
||||||
|
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
|
||||||
|
* @param y vertical position, value from -1.0 ... 0 .. 1.0
|
||||||
|
*/
|
||||||
|
void onMovement(float x, float y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onClick event will be fired on click on the analog stick
|
||||||
|
*/
|
||||||
|
void onClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onDoubleClick event will be fired on a double click in a short time frame on the analog
|
||||||
|
* stick.
|
||||||
|
*/
|
||||||
|
void onDoubleClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onRevoke event will be fired on unpress of the analog stick.
|
||||||
|
*/
|
||||||
|
void onRevoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement states of the analog sick.
|
||||||
|
*/
|
||||||
|
private enum STICK_STATE {
|
||||||
|
NO_MOVEMENT,
|
||||||
|
MOVED_IN_DEAD_ZONE,
|
||||||
|
MOVED_ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click type states.
|
||||||
|
*/
|
||||||
|
private enum CLICK_STATE {
|
||||||
|
SINGLE,
|
||||||
|
DOUBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* configuration if the analog stick should be displayed as circle or square
|
||||||
|
*/
|
||||||
|
private boolean circle_stick = true; // TODO: implement square sick for simulations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* outer radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_complete = 0;
|
||||||
|
/**
|
||||||
|
* analog stick radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_analog_stick = 0;
|
||||||
|
/**
|
||||||
|
* dead zone radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_dead_zone = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* horizontal position in relation to the center of the element
|
||||||
|
*/
|
||||||
|
private float relative_x = 0;
|
||||||
|
/**
|
||||||
|
* vertical position in relation to the center of the element
|
||||||
|
*/
|
||||||
|
private float relative_y = 0;
|
||||||
|
|
||||||
|
|
||||||
|
private double movement_radius = 0;
|
||||||
|
private double movement_angle = 0;
|
||||||
|
|
||||||
|
private float position_stick_x = 0;
|
||||||
|
private float position_stick_y = 0;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
|
||||||
|
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
|
||||||
|
|
||||||
|
private List<AnalogStickListener> listeners = new ArrayList<>();
|
||||||
|
private long timeLastClick = 0;
|
||||||
|
|
||||||
|
private static double getMovementRadius(float x, float y) {
|
||||||
|
return Math.sqrt(x * x + y * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double getAngle(float way_x, float way_y) {
|
||||||
|
// prevent divisions by zero for corner cases
|
||||||
|
if (way_x == 0) {
|
||||||
|
return way_y < 0 ? Math.PI : 0;
|
||||||
|
} else if (way_y == 0) {
|
||||||
|
if (way_x > 0) {
|
||||||
|
return Math.PI * 3 / 2;
|
||||||
|
} else if (way_x < 0) {
|
||||||
|
return Math.PI * 1 / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return correct calculated angle for each quadrant
|
||||||
|
if (way_x > 0) {
|
||||||
|
if (way_y < 0) {
|
||||||
|
// first quadrant
|
||||||
|
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
|
||||||
|
} else {
|
||||||
|
// second quadrant
|
||||||
|
return Math.PI + Math.atan((double) (way_x / way_y));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (way_y > 0) {
|
||||||
|
// third quadrant
|
||||||
|
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
|
||||||
|
} else {
|
||||||
|
// fourth quadrant
|
||||||
|
return 0 + Math.atan((double) (-way_x / -way_y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnalogStick(VirtualController controller, Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
// reset stick position
|
||||||
|
position_stick_x = getWidth() / 2;
|
||||||
|
position_stick_y = getHeight() / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAnalogStickListener(AnalogStickListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnMovement(float x, float y) {
|
||||||
|
_DBG("movement x: " + x + " movement y: " + y);
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onMovement(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnClick() {
|
||||||
|
_DBG("click");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnDoubleClick() {
|
||||||
|
_DBG("double click");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onDoubleClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnRevoke() {
|
||||||
|
_DBG("revoke");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onRevoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
// calculate new radius sizes depending
|
||||||
|
radius_complete = getPercent(getCorrectWidth() / 2, 90);
|
||||||
|
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||||
|
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||||
|
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
// draw outer circle
|
||||||
|
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
} else {
|
||||||
|
paint.setColor(pressedColor);
|
||||||
|
}
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
|
||||||
|
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
// draw dead zone
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
|
||||||
|
|
||||||
|
// draw stick depending on state
|
||||||
|
switch (stick_state) {
|
||||||
|
case NO_MOVEMENT: {
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MOVED_IN_DEAD_ZONE:
|
||||||
|
case MOVED_ACTIVE: {
|
||||||
|
paint.setColor(pressedColor);
|
||||||
|
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePosition() {
|
||||||
|
// get 100% way
|
||||||
|
float complete = radius_complete - radius_analog_stick;
|
||||||
|
|
||||||
|
// calculate relative way
|
||||||
|
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||||
|
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||||
|
|
||||||
|
// update positions
|
||||||
|
position_stick_x = getWidth() / 2 - correlated_x;
|
||||||
|
position_stick_y = getHeight() / 2 - correlated_y;
|
||||||
|
|
||||||
|
// Stay active even if we're back in the deadzone because we know the user is actively
|
||||||
|
// giving analog stick input and we don't want to snap back into the deadzone.
|
||||||
|
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
||||||
|
// them to make precise movements.
|
||||||
|
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
||||||
|
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
||||||
|
movement_radius > radius_dead_zone) ?
|
||||||
|
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
|
|
||||||
|
// trigger move event if state active
|
||||||
|
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
|
||||||
|
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onElementTouchEvent(MotionEvent event) {
|
||||||
|
// save last click state
|
||||||
|
CLICK_STATE lastClickState = click_state;
|
||||||
|
|
||||||
|
// get absolute way for each axis
|
||||||
|
relative_x = -(getWidth() / 2 - event.getX());
|
||||||
|
relative_y = -(getHeight() / 2 - event.getY());
|
||||||
|
|
||||||
|
// get radius and angel of movement from center
|
||||||
|
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||||
|
movement_angle = getAngle(relative_x, relative_y);
|
||||||
|
|
||||||
|
// chop radius if out of outer circle and already pressed
|
||||||
|
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||||
|
// not pressed already, so ignore event from outer circle
|
||||||
|
if (!isPressed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
movement_radius = radius_complete - radius_analog_stick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle event depending on action
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
// down event (touch event)
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
// set to dead zoned, will be corrected in update position if necessary
|
||||||
|
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
|
// check for double click
|
||||||
|
if (lastClickState == CLICK_STATE.SINGLE &&
|
||||||
|
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
||||||
|
click_state = CLICK_STATE.DOUBLE;
|
||||||
|
notifyOnDoubleClick();
|
||||||
|
} else {
|
||||||
|
click_state = CLICK_STATE.SINGLE;
|
||||||
|
notifyOnClick();
|
||||||
|
}
|
||||||
|
// reset last click timestamp
|
||||||
|
timeLastClick = System.currentTimeMillis();
|
||||||
|
// set item pressed and update
|
||||||
|
setPressed(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// up event (revoke touch)
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
setPressed(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed()) {
|
||||||
|
// when is pressed calculate new positions (will trigger movement if necessary)
|
||||||
|
updatePosition();
|
||||||
|
} else {
|
||||||
|
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||||
|
notifyOnRevoke();
|
||||||
|
// not longer pressed reset analog stick
|
||||||
|
notifyOnMovement(0, 0);
|
||||||
|
}
|
||||||
|
// refresh view
|
||||||
|
invalidate();
|
||||||
|
// accept the touch event
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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 layer, Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
this.layer = layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDigitalButtonListener(DigitalButtonListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setText(String text) {
|
||||||
|
this.text = text;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(int id) {
|
||||||
|
this.icon = id;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setTextSize(getPercent(getWidth(), 30));
|
||||||
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||||
|
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
||||||
|
|
||||||
|
if (icon != -1) {
|
||||||
|
Drawable d = getResources().getDrawable(icon);
|
||||||
|
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
|
||||||
|
d.draw(canvas);
|
||||||
|
} else {
|
||||||
|
paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
|
||||||
|
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickCallback() {
|
||||||
|
_DBG("clicked");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
timerLongClick = new Timer();
|
||||||
|
longClickTimerTask = new TimerLongClickTimerTask();
|
||||||
|
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLongClickCallback() {
|
||||||
|
_DBG("long click");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onLongClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onReleaseCallback() {
|
||||||
|
_DBG("released");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onRelease();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDigitalPadListener(DigitalPadListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setTextSize(getPercent(getCorrectWidth(), 20));
|
||||||
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||||
|
// draw no direction rect
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
|
||||||
|
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw left rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||||
|
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// draw up rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw right rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||||
|
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw down rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||||
|
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw left up line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||||
|
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw up right line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw right down line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
|
||||||
|
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw down left line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void newDirectionCallback(int direction) {
|
||||||
|
_DBG("direction: " + direction);
|
||||||
|
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalPadListener listener : listeners) {
|
||||||
|
listener.onDirectionChange(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onElementTouchEvent(MotionEvent event) {
|
||||||
|
// get masked (not specific to a pointer) action
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
direction = 0;
|
||||||
|
|
||||||
|
if (event.getX() < getPercent(getWidth(), 33)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_LEFT;
|
||||||
|
}
|
||||||
|
if (event.getX() > getPercent(getWidth(), 66)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
|
||||||
|
}
|
||||||
|
if (event.getY() > getPercent(getHeight(), 66)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_DOWN;
|
||||||
|
}
|
||||||
|
if (event.getY() < getPercent(getHeight(), 33)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_UP;
|
||||||
|
}
|
||||||
|
newDirectionCallback(direction);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
direction = 0;
|
||||||
|
newDirectionCallback(direction);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface DigitalPadListener {
|
||||||
|
void onDirectionChange(int direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
public class LeftAnalogStick extends AnalogStick {
|
||||||
|
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
|
||||||
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
|
@Override
|
||||||
|
public void onMovement(float x, float y) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftStickX = (short) (x * 0x7FFE);
|
||||||
|
inputContext.leftStickY = (short) (y * 0x7FFE);
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDoubleClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevoke() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class LeftTrigger extends DigitalButton {
|
||||||
|
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
|
super(controller, layer, context);
|
||||||
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftTrigger = (byte) 0xFF;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftTrigger = (byte) 0x00;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
public class RightAnalogStick extends AnalogStick {
|
||||||
|
public RightAnalogStick(final VirtualController controller, final Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
|
||||||
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
|
@Override
|
||||||
|
public void onMovement(float x, float y) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightStickX = (short) (x * 0x7FFE);
|
||||||
|
inputContext.rightStickY = (short) (y * 0x7FFE);
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDoubleClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevoke() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class RightTrigger extends DigitalButton {
|
||||||
|
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
|
super(controller, layer, context);
|
||||||
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightTrigger = (byte) 0xFF;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightTrigger = (byte) 0x00;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class VirtualController {
|
||||||
|
public class ControllerInputContext {
|
||||||
|
public short inputMap = 0x0000;
|
||||||
|
public byte leftTrigger = 0x00;
|
||||||
|
public byte rightTrigger = 0x00;
|
||||||
|
public short rightStickX = 0x0000;
|
||||||
|
public short rightStickY = 0x0000;
|
||||||
|
public short leftStickX = 0x0000;
|
||||||
|
public short leftStickY = 0x0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ControllerMode {
|
||||||
|
Active,
|
||||||
|
Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
|
private NvConnection connection = null;
|
||||||
|
private Context context = null;
|
||||||
|
|
||||||
|
private FrameLayout frame_layout = null;
|
||||||
|
private RelativeLayout relative_layout = null;
|
||||||
|
|
||||||
|
ControllerMode currentMode = ControllerMode.Active;
|
||||||
|
ControllerInputContext inputContext = new ControllerInputContext();
|
||||||
|
|
||||||
|
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
||||||
|
private Button buttonConfigure = null;
|
||||||
|
|
||||||
|
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||||
|
|
||||||
|
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
||||||
|
this.connection = conn;
|
||||||
|
this.frame_layout = layout;
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
relative_layout = new RelativeLayout(context);
|
||||||
|
|
||||||
|
frame_layout.addView(relative_layout);
|
||||||
|
|
||||||
|
buttonConfigure = new Button(context);
|
||||||
|
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||||
|
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
String message;
|
||||||
|
|
||||||
|
if (currentMode == ControllerMode.Configuration) {
|
||||||
|
currentMode = ControllerMode.Active;
|
||||||
|
message = "Exiting configuration mode";
|
||||||
|
} else {
|
||||||
|
currentMode = ControllerMode.Configuration;
|
||||||
|
message = "Entering configuration mode";
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
relative_layout.invalidate();
|
||||||
|
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
element.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeElements() {
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
relative_layout.removeView(element);
|
||||||
|
}
|
||||||
|
elements.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
||||||
|
elements.add(element);
|
||||||
|
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
||||||
|
layoutParams.setMargins(x, y, 0, 0);
|
||||||
|
|
||||||
|
relative_layout.addView(element, layoutParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<VirtualControllerElement> getElements() {
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final void _DBG(String text) {
|
||||||
|
if (_PRINT_DEBUG_INFORMATION) {
|
||||||
|
System.out.println("VirtualController: " + text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshLayout() {
|
||||||
|
relative_layout.removeAllViews();
|
||||||
|
removeElements();
|
||||||
|
|
||||||
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
|
int buttonSize = (int)(screen.heightPixels*0.05f);
|
||||||
|
layoutParamsButtonConfigure = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
||||||
|
relative_layout.addView(buttonConfigure, layoutParamsButtonConfigure);
|
||||||
|
|
||||||
|
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControllerMode getControllerMode() {
|
||||||
|
return currentMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControllerInputContext getControllerInputContext() {
|
||||||
|
return inputContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendControllerInputContext() {
|
||||||
|
sendControllerInputPacket();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendControllerInputPacket() {
|
||||||
|
try {
|
||||||
|
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
||||||
|
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
||||||
|
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
||||||
|
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
||||||
|
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||||
|
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||||
|
|
||||||
|
if (connection != null) {
|
||||||
|
connection.sendControllerInput(
|
||||||
|
inputContext.inputMap,
|
||||||
|
inputContext.leftTrigger,
|
||||||
|
inputContext.rightTrigger,
|
||||||
|
inputContext.leftStickX,
|
||||||
|
inputContext.leftStickY,
|
||||||
|
inputContext.rightStickX,
|
||||||
|
inputContext.rightStickY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
public class VirtualControllerConfigurationLoader {
|
||||||
|
private static final String PROFILE_PATH = "profiles";
|
||||||
|
|
||||||
|
private static int getPercent(
|
||||||
|
int percent,
|
||||||
|
int total) {
|
||||||
|
return (int) (((float) total / (float) 100) * (float) percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalPad createDigitalPad(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
|
||||||
|
DigitalPad digitalPad = new DigitalPad(controller, context);
|
||||||
|
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
|
||||||
|
@Override
|
||||||
|
public void onDirectionChange(int direction) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
|
||||||
|
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.UP_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
|
}
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return digitalPad;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createDigitalButton(
|
||||||
|
final int keyShort,
|
||||||
|
final int keyLong,
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
DigitalButton button = new DigitalButton(controller, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
|
||||||
|
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= keyShort;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= keyLong;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~keyShort;
|
||||||
|
inputContext.inputMap &= ~keyLong;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createLeftTrigger(
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
LeftTrigger button = new LeftTrigger(controller, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createRightTrigger(
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
RightTrigger button = new RightTrigger(controller, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnalogStick createLeftStick(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
return new LeftAnalogStick(controller, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnalogStick createRightStick(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
return new RightAnalogStick(controller, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int BUTTON_BASE_X = 65;
|
||||||
|
private static final int BUTTON_BASE_Y = 5;
|
||||||
|
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
||||||
|
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
||||||
|
|
||||||
|
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||||
|
|
||||||
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
|
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
||||||
|
// into a single call. Due to floating point rounding, this isn't actually possible.
|
||||||
|
|
||||||
|
controller.addElement(createDigitalPad(controller, context),
|
||||||
|
getPercent(5, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(30, screen.widthPixels),
|
||||||
|
getPercent(40, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createLeftTrigger(
|
||||||
|
0, "LT", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createRightTrigger(
|
||||||
|
0, "RT", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createLeftStick(controller, context),
|
||||||
|
getPercent(5, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createRightStick(controller, context),
|
||||||
|
getPercent(55, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(90, screen.heightPixels),
|
||||||
|
getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(10, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||||
|
getPercent(40, screen.widthPixels)+getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(90, screen.heightPixels),
|
||||||
|
getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(10, screen.heightPixels)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOT IMPLEMENTED YET,
|
||||||
|
this should later be used to store and load a profile for the virtual controller
|
||||||
|
public static void saveProfile(final String name,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
|
||||||
|
SharedPreferences preferences = context.getSharedPreferences(PROFILE_PATH + "/" +
|
||||||
|
name, Activity.MODE_PRIVATE);
|
||||||
|
|
||||||
|
JSONArray elementConfigurations = new JSONArray();
|
||||||
|
for (VirtualControllerElement element : controller.getElements()) {
|
||||||
|
JSONObject elementConfiguration = new JSONObject();
|
||||||
|
try {
|
||||||
|
elementConfiguration.put("TYPE", element.getClass().getName());
|
||||||
|
elementConfiguration.put("CONFIGURATION", element.getConfiguration());
|
||||||
|
elementConfigurations.put(elementConfiguration);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPreferences.Editor editor= preferences.edit();
|
||||||
|
editor.putString("ELEMENTS", elementConfigurations.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadFromPreferences(final VirtualController controller) {
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
public abstract class VirtualControllerElement extends View {
|
||||||
|
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
|
protected VirtualController virtualController;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
private int normalColor = 0xF0888888;
|
||||||
|
protected int pressedColor = 0xF00000FF;
|
||||||
|
private int configNormalColor = 0xF0FF0000;
|
||||||
|
private int configSelectedColor = 0xF000FF00;
|
||||||
|
|
||||||
|
protected int startSize_x;
|
||||||
|
protected int startSize_y;
|
||||||
|
|
||||||
|
float position_pressed_x = 0;
|
||||||
|
float position_pressed_y = 0;
|
||||||
|
|
||||||
|
private enum Mode {
|
||||||
|
Normal,
|
||||||
|
Resize,
|
||||||
|
Move
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mode currentMode = Mode.Normal;
|
||||||
|
|
||||||
|
protected VirtualControllerElement(VirtualController controller, Context context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
this.virtualController = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
||||||
|
int newPos_x = (int) getX() + x - pressed_x;
|
||||||
|
int newPos_y = (int) getY() + y - pressed_y;
|
||||||
|
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
||||||
|
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
||||||
|
layoutParams.rightMargin = 0;
|
||||||
|
layoutParams.bottomMargin = 0;
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
int newHeight = height + (startSize_y - pressed_y);
|
||||||
|
int newWidth = width + (startSize_x - pressed_x);
|
||||||
|
|
||||||
|
layoutParams.height = newHeight > 20 ? newHeight : 20;
|
||||||
|
layoutParams.width = newWidth > 20 ? newWidth : 20;
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
onElementDraw(canvas);
|
||||||
|
|
||||||
|
if (currentMode != Mode.Normal) {
|
||||||
|
paint.setColor(configSelectedColor);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
|
||||||
|
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||||
|
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
|
||||||
|
paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
protected void actionShowNormalColorChooser() {
|
||||||
|
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(AmbilWarnaDialog dialog)
|
||||||
|
{}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||||
|
normalColor = color;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
colorDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionShowPressedColorChooser() {
|
||||||
|
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(AmbilWarnaDialog dialog) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||||
|
pressedColor = color;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
colorDialog.show();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected void actionEnableMove() {
|
||||||
|
currentMode = Mode.Move;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionEnableResize() {
|
||||||
|
currentMode = Mode.Resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionCancel() {
|
||||||
|
currentMode = Mode.Normal;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getDefaultColor() {
|
||||||
|
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
||||||
|
configNormalColor : normalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getDefaultStrokeWidth() {
|
||||||
|
DisplayMetrics screen = getResources().getDisplayMetrics();
|
||||||
|
return (int)(screen.heightPixels*0.004f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void showConfigurationDialog() {
|
||||||
|
try {
|
||||||
|
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
||||||
|
|
||||||
|
alertBuilder.setTitle("Configuration");
|
||||||
|
|
||||||
|
CharSequence functions[] = new CharSequence[]{
|
||||||
|
"Move",
|
||||||
|
"Resize",
|
||||||
|
/*election
|
||||||
|
"Set n
|
||||||
|
Disable color sormal color",
|
||||||
|
"Set pressed color",
|
||||||
|
*/
|
||||||
|
"Cancel"
|
||||||
|
};
|
||||||
|
|
||||||
|
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which) {
|
||||||
|
case 0: { // move
|
||||||
|
actionEnableMove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: { // resize
|
||||||
|
actionEnableResize();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
case 2: { // set default color
|
||||||
|
actionShowNormalColorChooser();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: { // set pressed color
|
||||||
|
actionShowPressedColorChooser();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
default: { // cancel
|
||||||
|
actionCancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AlertDialog alert = alertBuilder.create();
|
||||||
|
// show menu
|
||||||
|
alert.show();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
|
||||||
|
return onElementTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
position_pressed_x = event.getX();
|
||||||
|
position_pressed_y = event.getY();
|
||||||
|
startSize_x = getWidth();
|
||||||
|
startSize_y = getHeight();
|
||||||
|
|
||||||
|
actionEnableMove();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
switch (currentMode) {
|
||||||
|
case Move: {
|
||||||
|
moveElement(
|
||||||
|
(int) position_pressed_x,
|
||||||
|
(int) position_pressed_y,
|
||||||
|
(int) event.getX(),
|
||||||
|
(int) event.getY());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Resize: {
|
||||||
|
resizeElement(
|
||||||
|
(int) position_pressed_x,
|
||||||
|
(int) position_pressed_y,
|
||||||
|
(int) event.getX(),
|
||||||
|
(int) event.getY());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Normal: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
actionCancel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected void onElementDraw(Canvas canvas);
|
||||||
|
|
||||||
|
abstract public boolean onElementTouchEvent(MotionEvent event);
|
||||||
|
|
||||||
|
protected static final void _DBG(String text) {
|
||||||
|
if (_PRINT_DEBUG_INFORMATION) {
|
||||||
|
System.out.println(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColors(int normalColor, int pressedColor) {
|
||||||
|
this.normalColor = normalColor;
|
||||||
|
this.pressedColor = pressedColor;
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final float getPercent(float value, float percent) {
|
||||||
|
return value / 100 * percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final int getCorrectWidth() {
|
||||||
|
return getWidth() > getHeight() ? getHeight() : getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
public JSONObject getConfiguration () {
|
||||||
|
JSONObject configuration = new JSONObject();
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadConfiguration (JSONObject configuration) {
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -32,9 +32,11 @@ public class MediaCodecHelper {
|
|||||||
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;
|
||||||
|
|
||||||
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,45 +47,91 @@ 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 emulator from this restriction.
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
|
||||||
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
|
||||||
|
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
||||||
|
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
||||||
|
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
|
||||||
|
|
||||||
|
// 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>();
|
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||||
|
|
||||||
constrainedHighProfilePrefixes = new LinkedList<String>();
|
constrainedHighProfilePrefixes = new LinkedList<>();
|
||||||
constrainedHighProfilePrefixes.add("omx.intel");
|
constrainedHighProfilePrefixes.add("omx.intel");
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
whitelistedHevcDecoders = new LinkedList<>();
|
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");
|
whitelistedHevcDecoders.add("omx.exynos");
|
||||||
// whitelistedHevcDecoders.add("omx.nvidia"); TODO: This needs a similar fixup to the Tegra 3
|
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
||||||
whitelistedHevcDecoders.add("omx.amlogic");
|
//whitelistedHevcDecoders.add("omx.nvidia");
|
||||||
whitelistedHevcDecoders.add("omx.rk");
|
|
||||||
// omx.qcom added conditionally during initialization
|
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
||||||
|
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
|
||||||
|
// to actually be tested. Ugh...
|
||||||
|
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||||
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
|
}
|
||||||
|
|
||||||
|
// These theoretically have good HEVC decoding capabilities (potentially better than
|
||||||
|
// their AVC decoders), but haven't been tested enough
|
||||||
|
//whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
|
//whitelistedHevcDecoders.add("omx.rk");
|
||||||
|
|
||||||
|
// Based on GPU attributes queried at runtime, the omx.qcom prefix will be added
|
||||||
|
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initializeWithContext(Context context) {
|
public static void initializeWithContext(Context context) {
|
||||||
@@ -91,20 +139,40 @@ public class MediaCodecHelper {
|
|||||||
(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) {
|
||||||
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
|
||||||
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
|
||||||
// 3xx - bad
|
|
||||||
// 4xx - good
|
|
||||||
//
|
|
||||||
// Unfortunately, it's not that easy to get that information here, so I'll use an
|
|
||||||
// approximation by checking the GLES level (<= 3.0 is bad).
|
|
||||||
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
||||||
if (configInfo.reqGlEsVersion > 0x30000) {
|
|
||||||
LimeLog.info("Added omx.qcom to supported decoders based on GLES 3.1+ support");
|
// Tegra K1 and later can do reference frame invalidation properly
|
||||||
whitelistedHevcDecoders.add("omx.qcom");
|
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
|
||||||
|
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
||||||
|
// 3xx - bad
|
||||||
|
// 4xx - good
|
||||||
|
//
|
||||||
|
// Unfortunately, it's not that easy to get that information here, so I'll use an
|
||||||
|
// approximation by checking the GLES level (<= 3.0 is bad).
|
||||||
|
if (configInfo.reqGlEsVersion > 0x30000) {
|
||||||
|
// FIXME: We prefer reference frame invalidation support (which is only doable on AVC on
|
||||||
|
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
||||||
|
// to force HEVC on.
|
||||||
|
//LimeLog.info("Added omx.qcom to supported HEVC decoders based on GLES 3.1+ support");
|
||||||
|
//whitelistedHevcDecoders.add("omx.qcom");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
for (String badPrefix : decoderList) {
|
for (String badPrefix : decoderList) {
|
||||||
@@ -168,6 +236,14 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName) {
|
||||||
|
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) {
|
||||||
|
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
|
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
|
||||||
// TODO: Shield Tablet K1/LTE?
|
// TODO: Shield Tablet K1/LTE?
|
||||||
//
|
//
|
||||||
@@ -190,13 +266,23 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -354,7 +440,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();
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public class ComputerDatabaseManager {
|
|||||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||||
private static final String MAC_COLUMN_NAME = "Mac";
|
private static final String MAC_COLUMN_NAME = "Mac";
|
||||||
|
|
||||||
|
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
public ComputerDatabaseManager(Context c) {
|
public ComputerDatabaseManager(Context c) {
|
||||||
@@ -53,57 +55,81 @@ public class ComputerDatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteComputer(String name) {
|
public void deleteComputer(String name) {
|
||||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
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(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||||
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
|
||||||
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
|
||||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||||
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.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 local address 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;
|
||||||
|
|
||||||
|
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);
|
|
||||||
|
|
||||||
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 a field is corrupt or missing, skip the database entry
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
||||||
details.macAddress == null) {
|
details.macAddress == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -118,47 +144,18 @@ public class ComputerDatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails getComputerByName(String name) {
|
public ComputerDetails getComputerByName(String name) {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
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;
|
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
|
|
||||||
// If a field is corrupt or missing, delete the database entry
|
// If a field is corrupt or missing, delete the database entry
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
||||||
details.macAddress == 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,7 +3,6 @@ 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.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -20,6 +19,7 @@ import com.limelight.nvstream.http.NvHTTP;
|
|||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
import com.limelight.utils.CacheHelper;
|
import 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;
|
||||||
@@ -37,6 +37,9 @@ public class ComputerManagerService extends Service {
|
|||||||
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 = 500;
|
||||||
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 +47,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,12 +78,15 @@ 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;
|
||||||
@@ -135,6 +141,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 +154,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
|
t.setName("Polling thread for " + tuple.computer.localAddress);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +171,18 @@ 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;
|
||||||
|
tuple.computer.reachability = ComputerDetails.Reachability.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 +210,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) {
|
||||||
@@ -228,12 +242,29 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void invalidateStateForComputer(UUID uuid) {
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (uuid.equals(tuple.computer.uuid)) {
|
||||||
|
// We need the network lock to prevent a concurrent poll
|
||||||
|
// from wiping this change out
|
||||||
|
synchronized (tuple.networkLock) {
|
||||||
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onUnbind(Intent intent) {
|
public boolean onUnbind(Intent intent) {
|
||||||
// Stop mDNS autodiscovery
|
if (discoveryBinder != null) {
|
||||||
discoveryBinder.stopDiscovery();
|
// Stop mDNS autodiscovery
|
||||||
|
discoveryBinder.stopDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
pollingActive = false;
|
pollingActive = false;
|
||||||
@@ -258,7 +289,7 @@ public class ComputerManagerService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
// Kick off a serverinfo poll on this machine
|
// Kick off a serverinfo poll on this machine
|
||||||
addComputerBlocking(computer.getAddress());
|
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -274,15 +305,22 @@ public class ComputerManagerService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTuple(ComputerDetails details) {
|
private void addTuple(ComputerDetails details, boolean manuallyAdded) {
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
// Check if this is the same computer
|
// Check if this is the same computer
|
||||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||||
// Update details anyway in case this machine has been re-added by IP
|
if (manuallyAdded) {
|
||||||
// after not being reachable by our existing information
|
// Update details anyway in case this machine has been re-added by IP
|
||||||
tuple.computer.localIp = details.localIp;
|
// after not being reachable by our existing information
|
||||||
tuple.computer.remoteIp = details.remoteIp;
|
tuple.computer.localAddress = details.localAddress;
|
||||||
|
tuple.computer.remoteAddress = details.remoteAddress;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This indicates that mDNS discovered this address, so we
|
||||||
|
// should only apply the local address.
|
||||||
|
tuple.computer.localAddress = details.localAddress;
|
||||||
|
}
|
||||||
|
|
||||||
// Start a polling thread if polling is active
|
// Start a polling thread if polling is active
|
||||||
if (pollingActive && tuple.thread == null) {
|
if (pollingActive && tuple.thread == null) {
|
||||||
@@ -307,11 +345,11 @@ 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.localAddress = addr;
|
||||||
fakeDetails.remoteIp = addr;
|
fakeDetails.remoteAddress = addr;
|
||||||
|
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
try {
|
try {
|
||||||
@@ -325,7 +363,7 @@ public class ComputerManagerService extends Service {
|
|||||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||||
|
|
||||||
// Start a polling thread for this machine
|
// Start a polling thread for this machine
|
||||||
addTuple(fakeDetails);
|
addTuple(fakeDetails, manuallyAdded);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -373,14 +411,14 @@ 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(),
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
ComputerDetails newDetails = http.getComputerDetails();
|
ComputerDetails newDetails = http.getComputerDetails();
|
||||||
@@ -401,10 +439,10 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
// Just try to establish a TCP connection to speculatively detect a running
|
// Just try to establish a TCP connection to speculatively detect a running
|
||||||
// GFE server
|
// GFE server
|
||||||
private boolean fastPollIp(InetAddress addr) {
|
private boolean fastPollIp(String address) {
|
||||||
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 +450,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 +464,16 @@ 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 ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) 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];
|
||||||
|
|
||||||
startFastPollThread(local, localInfo);
|
startFastPollThread(localAddress, localInfo);
|
||||||
startFastPollThread(remote, remoteInfo);
|
startFastPollThread(remoteAddress, remoteInfo);
|
||||||
|
|
||||||
// Check local first
|
// Check local first
|
||||||
synchronized (localInfo) {
|
synchronized (localInfo) {
|
||||||
@@ -468,13 +506,13 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
// If the local address is routable across the Internet,
|
// If the local address is routable across the Internet,
|
||||||
// always consider this PC remote to be conservative
|
// always consider this PC remote to be conservative
|
||||||
if (details.localIp.equals(details.remoteIp)) {
|
if (details.localAddress.equals(details.remoteAddress)) {
|
||||||
reachability = ComputerDetails.Reachability.REMOTE;
|
reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
// 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+")");
|
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +")");
|
||||||
reachability = fastPollPc(details.localIp, details.remoteIp);
|
reachability = fastPollPc(details.localAddress, details.remoteAddress);
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
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 no connection could be established to either IP address, there's nothing we can do
|
||||||
@@ -486,39 +524,39 @@ public class ComputerManagerService extends Service {
|
|||||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
|
||||||
if (localFirst) {
|
if (localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
polledDetails = tryPollIp(details, details.localAddress);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
polledDetails = tryPollIp(details, details.remoteAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
InetAddress reachableAddr = null;
|
String reachableAddr = null;
|
||||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
|
||||||
// Failed, so let's try the fallback
|
// Failed, so let's try the fallback
|
||||||
if (!localFirst) {
|
if (!localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
polledDetails = tryPollIp(details, details.localAddress);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
polledDetails = tryPollIp(details, details.remoteAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (polledDetails != null) {
|
if (polledDetails != null) {
|
||||||
// The fallback poll worked
|
// The fallback poll worked
|
||||||
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
|
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (polledDetails != null) {
|
else if (polledDetails != null) {
|
||||||
reachableAddr = localFirst ? details.localIp : details.remoteIp;
|
reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reachableAddr == null) {
|
if (reachableAddr == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (polledDetails.remoteIp.equals(reachableAddr)) {
|
if (polledDetails.remoteAddress.equals(reachableAddr)) {
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else if (polledDetails.localIp.equals(reachableAddr)) {
|
else if (polledDetails.localAddress.equals(reachableAddr)) {
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -540,7 +578,7 @@ public class ComputerManagerService extends Service {
|
|||||||
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
||||||
if (confirmationReachTuple == null) {
|
if (confirmationReachTuple == null) {
|
||||||
// Neither of those seem to work, so we'll hold onto the address that did work
|
// Neither of those seem to work, so we'll hold onto the address that did work
|
||||||
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
|
initialReachTuple.computer.localAddress = initialReachTuple.reachableAddress;
|
||||||
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -550,8 +588,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the old MAC address
|
// Save some details about the old state of the PC that we may wish
|
||||||
|
// to restore later.
|
||||||
String savedMacAddress = details.macAddress;
|
String savedMacAddress = details.macAddress;
|
||||||
|
String savedLocalAddress = details.localAddress;
|
||||||
|
String savedRemoteAddress = details.remoteAddress;
|
||||||
|
|
||||||
// If we got here, it's reachable
|
// If we got here, it's reachable
|
||||||
details.update(initialReachTuple.computer);
|
details.update(initialReachTuple.computer);
|
||||||
@@ -562,6 +603,33 @@ public class ComputerManagerService extends Service {
|
|||||||
details.macAddress = savedMacAddress;
|
details.macAddress = savedMacAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We never want to lose IP addresses by polling server info. If we get a poll back
|
||||||
|
// where localAddress == remoteAddress but savedLocalAddress != savedRemoteAddress,
|
||||||
|
// then we've lost an address in the polling and we should restore the one that's missing.
|
||||||
|
if (details.localAddress.equals(details.remoteAddress) &&
|
||||||
|
!savedLocalAddress.equals(savedRemoteAddress)) {
|
||||||
|
if (details.localAddress.equals(savedLocalAddress)) {
|
||||||
|
// Local addresses are identical, so put the old remote address back
|
||||||
|
details.remoteAddress = savedRemoteAddress;
|
||||||
|
}
|
||||||
|
else if (details.remoteAddress.equals(savedRemoteAddress)) {
|
||||||
|
// Remote addresses are identical, so put the old local address back
|
||||||
|
details.localAddress = savedLocalAddress;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Neither IP address match. Let's restore the remote address to be safe.
|
||||||
|
details.remoteAddress = savedRemoteAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now update the reachability so the correct address is used
|
||||||
|
if (details.localAddress.equals(initialReachTuple.reachableAddress)) {
|
||||||
|
details.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
details.reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,7 +653,7 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||||
// Add tuples for each computer
|
// Add tuples for each computer
|
||||||
addTuple(computer);
|
addTuple(computer, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
@@ -661,9 +729,8 @@ 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
|
// Can't poll if it's not online
|
||||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
@@ -677,19 +744,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(),
|
||||||
|
null, 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,7 +765,15 @@ 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 != null && !appList.isEmpty() &&
|
||||||
|
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||||
// Open the cache file
|
// Open the cache file
|
||||||
OutputStream cacheOut = null;
|
OutputStream cacheOut = null;
|
||||||
try {
|
try {
|
||||||
@@ -721,6 +789,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 +804,8 @@ public class ComputerManagerService extends Service {
|
|||||||
listener.notifyComputerUpdated(computer);
|
listener.notifyComputerUpdated(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if (appList == null || 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();
|
||||||
@@ -761,6 +834,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 +844,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.limelight.grid;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.graphics.BitmapFactory;
|
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 +15,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 +27,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;
|
||||||
@@ -53,9 +52,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
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.getCacheDir()));
|
||||||
BitmapFactory.decodeResource(activity.getResources(),
|
|
||||||
R.drawable.image_loading, options));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
public void cancelQueuedOperations() {
|
||||||
@@ -68,7 +65,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 +83,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 +101,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,7 +1,9 @@
|
|||||||
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;
|
||||||
@@ -14,7 +16,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 +28,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 +38,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.reachability == ComputerDetails.Reachability.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 +72,11 @@ 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;
|
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 {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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;
|
||||||
|
|||||||
@@ -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,9 @@ 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, null, PlatformBinding.getCryptoProvider(context));
|
||||||
in = http.getBoxArt(tuple.app);
|
in = http.getBoxArt(tuple.app);
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
@@ -37,13 +35,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,5 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -17,7 +14,6 @@ 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;
|
||||||
@@ -29,7 +25,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) {
|
||||||
@@ -50,18 +46,12 @@ public class AddComputerManually extends Activity {
|
|||||||
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 {
|
if (!managerBinder.addComputerBlocking(host, true)){
|
||||||
InetAddress addr = InetAddress.getByName(host);
|
msg = getResources().getString(R.string.addpc_fail);
|
||||||
|
}
|
||||||
if (!managerBinder.addComputerBlocking(addr)){
|
else {
|
||||||
msg = getResources().getString(R.string.addpc_fail);
|
msg = getResources().getString(R.string.addpc_success);
|
||||||
}
|
finish = true;
|
||||||
else {
|
|
||||||
msg = getResources().getString(R.string.addpc_success);
|
|
||||||
finish = true;
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
msg = getResources().getString(R.string.addpc_unknown_host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
@@ -136,18 +126,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
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ 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 BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
|
||||||
|
|
||||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||||
@@ -42,6 +44,8 @@ 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 DEFAULT_BATTERY_SAVER = 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,6 +58,8 @@ 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 batterySaver;
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
public static int getDefaultBitrate(String resFpsString) {
|
||||||
if (resFpsString.equals("720p30")) {
|
if (resFpsString.equals("720p30")) {
|
||||||
@@ -97,7 +103,7 @@ 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) {
|
||||||
@@ -124,6 +130,16 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void resetStreamingSettings(Context context) {
|
||||||
|
// We consider resolution, FPS, bitrate, and video format as "streaming settings" here
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
prefs.edit()
|
||||||
|
.remove(BITRATE_PREF_STRING)
|
||||||
|
.remove(RES_FPS_PREF_STRING)
|
||||||
|
.remove(VIDEO_FORMAT_PREF_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();
|
||||||
@@ -183,6 +199,8 @@ 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.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,18 @@ 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.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
@@ -24,11 +23,7 @@ public class StreamSettings extends Activity {
|
|||||||
|
|
||||||
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(
|
getFragmentManager().beginTransaction().replace(
|
||||||
@@ -60,6 +55,15 @@ public class StreamSettings extends Activity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
PreferenceScreen screen = getPreferenceScreen();
|
||||||
|
|
||||||
|
// hide on-screen controls category on non touch screen devices
|
||||||
|
if (!getActivity().getPackageManager().
|
||||||
|
hasSystemFeature("android.hardware.touchscreen")) {
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_onscreen_controls");
|
||||||
|
screen.removePreference(category);
|
||||||
|
}
|
||||||
|
|
||||||
// Add a listener to the FPS and resolution preference
|
// Add a listener to the FPS and resolution preference
|
||||||
// so the bitrate can be auto-adjusted
|
// so the bitrate can be auto-adjusted
|
||||||
|
|||||||
@@ -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,55 @@
|
|||||||
|
package com.limelight.ui;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
|
||||||
|
public class StreamView extends SurfaceView {
|
||||||
|
private double desiredAspectRatio;
|
||||||
|
|
||||||
|
public void setDesiredAspectRatio(double aspectRatio) {
|
||||||
|
this.desiredAspectRatio = aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(21)
|
||||||
|
public StreamView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
// If no fixed aspect ratio has been provided, simply use the default onMeasure() behavior
|
||||||
|
if (desiredAspectRatio == 0) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on code from: https://www.buzzingandroid.com/2012/11/easy-measuring-of-custom-views-with-specific-aspect-ratio/
|
||||||
|
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
|
||||||
|
int measuredHeight, measuredWidth;
|
||||||
|
if (widthSize > heightSize * desiredAspectRatio) {
|
||||||
|
measuredHeight = heightSize;
|
||||||
|
measuredWidth = (int)(measuredHeight * desiredAspectRatio);
|
||||||
|
} else {
|
||||||
|
measuredWidth = widthSize;
|
||||||
|
measuredHeight = (int)(measuredWidth / desiredAspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ 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;
|
||||||
@@ -14,7 +17,7 @@ public class Dialog implements Runnable {
|
|||||||
|
|
||||||
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, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
@@ -55,7 +58,7 @@ public class Dialog implements Runnable {
|
|||||||
alert.setCancelable(false);
|
alert.setCancelable(false);
|
||||||
alert.setCanceledOnTouchOutside(false);
|
alert.setCanceledOnTouchOutside(false);
|
||||||
|
|
||||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
alert.setButton(AlertDialog.BUTTON_POSITIVE, activity.getResources().getText(android.R.string.ok), new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.remove(Dialog.this);
|
rundownDialogs.remove(Dialog.this);
|
||||||
@@ -67,6 +70,31 @@ public class Dialog implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
synchronized (rundownDialogs) {
|
||||||
|
rundownDialogs.remove(Dialog.this);
|
||||||
|
alert.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endAfterDismiss) {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
HelpLauncher.launchTroubleshooting(activity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
alert.setOnShowListener(new DialogInterface.OnShowListener(){
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShow(DialogInterface dialog) {
|
||||||
|
// Set focus to the OK button by default
|
||||||
|
Button button = alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||||
|
button.setFocusable(true);
|
||||||
|
button.setFocusableInTouchMode(true);
|
||||||
|
button.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.add(this);
|
rundownDialogs.add(this);
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.limelight.HelpActivity;
|
||||||
|
|
||||||
|
public class HelpLauncher {
|
||||||
|
|
||||||
|
private static void launchUrl(Context context, String url) {
|
||||||
|
// Try to launch the default browser
|
||||||
|
try {
|
||||||
|
// Fire TV devices will lie and say they do have a browser
|
||||||
|
// even though the OS just shows an error dialog if we
|
||||||
|
// try to use it.
|
||||||
|
if (!"Amazon".equalsIgnoreCase(Build.MANUFACTURER)) {
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse(url));
|
||||||
|
context.startActivity(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// This is only supposed to throw ActivityNotFoundException but
|
||||||
|
// it can (at least) also throw SecurityException if a user's default
|
||||||
|
// browser is not exported. We'll catch everything to workaround this.
|
||||||
|
|
||||||
|
// Fall through
|
||||||
|
}
|
||||||
|
|
||||||
|
// This platform has no browser (possibly a leanback device)
|
||||||
|
// We'll launch our WebView activity
|
||||||
|
Intent i = new Intent(context, HelpActivity.class);
|
||||||
|
i.setData(Uri.parse(url));
|
||||||
|
context.startActivity(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void launchSetupGuide(Context context) {
|
||||||
|
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void launchTroubleshooting(Context context) {
|
||||||
|
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,31 +14,35 @@ import com.limelight.nvstream.http.NvApp;
|
|||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
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.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
computer.localIp : computer.remoteIp;
|
computer.localAddress : computer.remoteAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
Intent intent = new Intent(parent, Game.class);
|
Intent intent = new Intent(parent, Game.class);
|
||||||
intent.putExtra(Game.EXTRA_HOST,
|
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
|
||||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
|
||||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
|
||||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||||
parent.startActivity(intent);
|
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
|
||||||
|
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
|
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
public static void doQuit(final Activity parent,
|
||||||
final InetAddress address,
|
final String address,
|
||||||
final NvApp app,
|
final NvApp app,
|
||||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
final Runnable onComplete) {
|
final Runnable onComplete) {
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ShortcutInfo;
|
||||||
|
import android.content.pm.ShortcutManager;
|
||||||
|
import android.graphics.drawable.Icon;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.limelight.AppView;
|
||||||
|
import com.limelight.AppViewShortcutTrampoline;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ShortcutHelper {
|
||||||
|
|
||||||
|
private final ShortcutManager sm;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public ShortcutHelper(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
sm = context.getSystemService(ShortcutManager.class);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sm = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private void reapShortcutsForDynamicAdd() {
|
||||||
|
List<ShortcutInfo> dynamicShortcuts = sm.getDynamicShortcuts();
|
||||||
|
while (dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) {
|
||||||
|
ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0);
|
||||||
|
for (ShortcutInfo scut : dynamicShortcuts) {
|
||||||
|
if (maxRankShortcut.getRank() < scut.getRank()) {
|
||||||
|
maxRankShortcut = scut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sm.removeDynamicShortcuts(Collections.singletonList(maxRankShortcut.getId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private List<ShortcutInfo> getAllShortcuts() {
|
||||||
|
LinkedList<ShortcutInfo> list = new LinkedList<>();
|
||||||
|
list.addAll(sm.getDynamicShortcuts());
|
||||||
|
list.addAll(sm.getPinnedShortcuts());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private ShortcutInfo getInfoForId(String id) {
|
||||||
|
List<ShortcutInfo> shortcuts = getAllShortcuts();
|
||||||
|
|
||||||
|
for (ShortcutInfo info : shortcuts) {
|
||||||
|
if (info.getId().equals(id)) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private boolean isExistingDynamicShortcut(String id) {
|
||||||
|
for (ShortcutInfo si : sm.getDynamicShortcuts()) {
|
||||||
|
if (si.getId().equals(id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportShortcutUsed(String id) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
ShortcutInfo sinfo = getInfoForId(id);
|
||||||
|
if (sinfo != null) {
|
||||||
|
sm.reportShortcutUsed(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
Intent i = new Intent(context, AppViewShortcutTrampoline.class);
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||||
|
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||||
|
i.setAction(Intent.ACTION_DEFAULT);
|
||||||
|
|
||||||
|
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
|
||||||
|
.setIntent(i)
|
||||||
|
.setShortLabel(computerName)
|
||||||
|
.setLongLabel(computerName)
|
||||||
|
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ShortcutInfo existingSinfo = getInfoForId(id);
|
||||||
|
if (existingSinfo != null) {
|
||||||
|
// Update in place
|
||||||
|
sm.updateShortcuts(Collections.singletonList(sinfo));
|
||||||
|
sm.enableShortcuts(Collections.singletonList(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reap shortcuts to make space for this if it's new
|
||||||
|
// NOTE: This CAN'T be an else on the above if, because it's
|
||||||
|
// possible that we have an existing shortcut but it's not a dynamic one.
|
||||||
|
if (!isExistingDynamicShortcut(id)) {
|
||||||
|
// To avoid a random carousel of shortcuts popping in and out based on polling status,
|
||||||
|
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
||||||
|
// to interact with this PC.
|
||||||
|
if (forceAdd || sm.getDynamicShortcuts().size() < sm.getMaxShortcutCountPerActivity()) {
|
||||||
|
reapShortcutsForDynamicAdd();
|
||||||
|
sm.addDynamicShortcuts(Collections.singletonList(sinfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
||||||
|
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableShortcut(String id, CharSequence reason) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
ShortcutInfo sinfo = getInfoForId(id);
|
||||||
|
if (sinfo != null) {
|
||||||
|
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import android.content.res.Configuration;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
@@ -16,6 +19,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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
@@ -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 */
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#include <stdlib.h>
|
|
||||||
#include <opus_multistream.h>
|
|
||||||
#include "nv_opus_dec.h"
|
|
||||||
|
|
||||||
OpusMSDecoder* decoder;
|
|
||||||
|
|
||||||
// This function must be called before
|
|
||||||
// any other decoding functions
|
|
||||||
int nv_opus_init(int sampleRate, int channelCount, int streams,
|
|
||||||
int coupledStreams, const unsigned char *mapping) {
|
|
||||||
int err;
|
|
||||||
decoder = opus_multistream_decoder_create(
|
|
||||||
sampleRate,
|
|
||||||
channelCount,
|
|
||||||
streams,
|
|
||||||
coupledStreams,
|
|
||||||
mapping,
|
|
||||||
&err);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function must be called after
|
|
||||||
// decoding is finished
|
|
||||||
void nv_opus_destroy(void) {
|
|
||||||
if (decoder != NULL) {
|
|
||||||
opus_multistream_decoder_destroy(decoder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// packets must be decoded in order
|
|
||||||
// a packet loss must call this function with NULL indata and 0 inlen
|
|
||||||
// returns the number of decoded samples
|
|
||||||
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata, int framesize) {
|
|
||||||
int err;
|
|
||||||
|
|
||||||
// Decoding to 16-bit PCM with FEC off
|
|
||||||
// Maximum length assuming 48KHz sample rate
|
|
||||||
err = opus_multistream_decode(decoder, indata, inlen,
|
|
||||||
outpcmdata, framesize, 0);
|
|
||||||
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
int nv_opus_init(int sampleRate, int channelCount, int streams,
|
|
||||||
int coupledStreams, const unsigned char *mapping);
|
|
||||||
void nv_opus_destroy(void);
|
|
||||||
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata, int framesize);
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#include "nv_opus_dec.h"
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
static int SamplesPerChannel;
|
|
||||||
static int ChannelCount;
|
|
||||||
|
|
||||||
// This function must be called before
|
|
||||||
// any other decoding functions
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_limelight_nvstream_av_audio_OpusDecoder_init(JNIEnv *env, jobject this, int sampleRate,
|
|
||||||
int samplesPerChannel, int channelCount, int streams,
|
|
||||||
int coupledStreams, jbyteArray mapping) {
|
|
||||||
jbyte* jni_mapping_data;
|
|
||||||
jint ret;
|
|
||||||
|
|
||||||
SamplesPerChannel = samplesPerChannel;
|
|
||||||
ChannelCount = channelCount;
|
|
||||||
|
|
||||||
jni_mapping_data = (*env)->GetByteArrayElements(env, mapping, 0);
|
|
||||||
ret = nv_opus_init(sampleRate, channelCount, streams, coupledStreams, jni_mapping_data);
|
|
||||||
(*env)->ReleaseByteArrayElements(env, mapping, jni_mapping_data, JNI_ABORT);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function must be called after
|
|
||||||
// decoding is finished
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_limelight_nvstream_av_audio_OpusDecoder_destroy(JNIEnv *env, jobject this) {
|
|
||||||
nv_opus_destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// packets must be decoded in order
|
|
||||||
// a packet loss must call this function with NULL indata and 0 inlen
|
|
||||||
// returns the number of decoded bytes
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_limelight_nvstream_av_audio_OpusDecoder_decode(
|
|
||||||
JNIEnv *env, jobject this, // JNI parameters
|
|
||||||
jbyteArray indata, jint inoff, jint inlen, // Input parameters
|
|
||||||
jbyteArray outpcmdata) // Output parameter
|
|
||||||
{
|
|
||||||
jint ret;
|
|
||||||
jbyte* jni_input_data;
|
|
||||||
jbyte* jni_pcm_data;
|
|
||||||
|
|
||||||
jni_pcm_data = (*env)->GetByteArrayElements(env, outpcmdata, 0);
|
|
||||||
if (indata != NULL) {
|
|
||||||
jni_input_data = (*env)->GetByteArrayElements(env, indata, 0);
|
|
||||||
|
|
||||||
ret = nv_opus_decode(&jni_input_data[inoff], inlen, (jshort*)jni_pcm_data, SamplesPerChannel);
|
|
||||||
|
|
||||||
// The input data isn't changed so it can be safely aborted
|
|
||||||
(*env)->ReleaseByteArrayElements(env, indata, jni_input_data, JNI_ABORT);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ret = nv_opus_decode(NULL, 0, (jshort*)jni_pcm_data, SamplesPerChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert samples (2 bytes) per channel to total bytes returned
|
|
||||||
if (ret > 0) {
|
|
||||||
ret *= ChannelCount * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*env)->ReleaseByteArrayElements(env, outpcmdata, jni_pcm_data, 0);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 40 KiB |