Compare commits
429 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cab0fa176e | |||
| 18c93abcb3 | |||
| bd64dfb661 | |||
| 82619063ee | |||
| 5dbf18d66e | |||
| 6a34ff2728 | |||
| f7c7487756 | |||
| f966cb4ca0 | |||
| 549563a3d2 | |||
| c5f2a3f8fe | |||
| 81a3bbd5e8 | |||
| 1509a2a799 | |||
| fc547b734f | |||
| b3700b5a19 | |||
| 2b29682095 | |||
| 286094ee33 | |||
| c7a061d24e | |||
| 4bdc2e0aba | |||
| e69061082b | |||
| 1da2ec3cb1 | |||
| 8ffc3b80b2 | |||
| 08f8b6cb8e | |||
| fb09c9692c | |||
| 4901b0c78f | |||
| 0a2117241f | |||
| f352cfd15b | |||
| ac7c5c1064 | |||
| 077cb2103d | |||
| cdeda011a4 | |||
| 894c146988 | |||
| 61cc9e151f | |||
| cfe4c9ff21 | |||
| d4bd29b320 | |||
| 7f2f2056c3 | |||
| 4dd3b2cfb7 | |||
| 2e62ad0f00 | |||
| 41ef292b82 | |||
| aa60671c88 | |||
| f1ccba39e8 | |||
| 226e580a30 | |||
| 6f8e719200 | |||
| c127af1e05 | |||
| 648904cc69 | |||
| dc85ddb3f9 | |||
| 23a7d8555f | |||
| bc9e250d34 | |||
| 2203186527 | |||
| 53d3d9ecb8 | |||
| de549f67a1 | |||
| 755c41481a | |||
| aebc2126bc | |||
| f43547fb31 | |||
| 398e4df7cf | |||
| ff68efc3f5 | |||
| 8ba2f51bda | |||
| 87b79b278b | |||
| 121e3ea9be | |||
| ec6ed79ee1 | |||
| ca125826a7 | |||
| dd0aecf108 | |||
| ef5cb2f0cd | |||
| e5a7bb40e9 | |||
| bfdda48fee | |||
| ebea1bb5c1 | |||
| 14bc1552fc | |||
| a5b80d3944 | |||
| 75d0eedc2b | |||
| 29ac7028fa | |||
| 8a63b61495 | |||
| eb9e6443e2 | |||
| 362c466a16 | |||
| 5dac42646b | |||
| c25faf6426 | |||
| 81df1245b4 | |||
| 2bf4d92185 | |||
| ae6073fe80 | |||
| d0463da2a1 | |||
| c0f8001627 | |||
| f39bf61b04 | |||
| 9c8237dab0 | |||
| b88251fa79 | |||
| 208855917e | |||
| 34bdf450e9 | |||
| 998fa1f4e9 | |||
| 5c80f7d58c | |||
| 7552181e24 | |||
| 4b2e26050e | |||
| 530b48de71 | |||
| f4721901f8 | |||
| 8b692269c1 | |||
| 079eca7b4d | |||
| fee40cdbe2 | |||
| 66920bb4cb | |||
| fdbf810aa2 | |||
| 08bfc1de4a | |||
| 76149328fe | |||
| 285f33f3f1 | |||
| b17c1b7588 | |||
| 5b25c90db8 | |||
| 931a0a5168 | |||
| f6a46438bd | |||
| 4a60ec1755 | |||
| ec222413dd | |||
| 5a28239813 | |||
| da45cba2ff | |||
| 54bc34496a | |||
| 294910ac84 | |||
| 71d2c6a5d5 | |||
| 79bf17fe24 | |||
| 31f66031bc | |||
| d3f2284791 | |||
| ec647608c4 | |||
| 597582ddd8 | |||
| c6d9889182 | |||
| 7c58234174 | |||
| ae9282b0af | |||
| 310ba646fc | |||
| d479908939 | |||
| 5cd5d68d22 | |||
| 3e0bf25acb | |||
| f3d277c94a | |||
| 04545ecbb0 | |||
| 5350651d6f | |||
| f2e2e28419 | |||
| b9031785ac | |||
| 91a72474a1 | |||
| b6e7c425c6 | |||
| 834ace4566 | |||
| 54af70005d | |||
| f2bf168925 | |||
| 27ffbd8dec | |||
| eaa82592fe | |||
| 73784585a8 | |||
| 262d562dd9 | |||
| ab4f904dc9 | |||
| fc4fdd5ee2 | |||
| 41c5b62b1a | |||
| 239cb0435c | |||
| c6ccc7a6e2 | |||
| 6cedb9019c | |||
| 8bc64f0438 | |||
| 89e6e39e58 | |||
| 645761f677 | |||
| 0fc60f7855 | |||
| ce38460d87 | |||
| de8e759d3a | |||
| 06f6134538 | |||
| ac352b3a23 | |||
| 9b8e65e552 | |||
| 35999a05f0 | |||
| 86ee30e9b4 | |||
| a81c4a1e23 | |||
| 394ce458a0 | |||
| f187e57899 | |||
| a15335872d | |||
| beb77b4dab | |||
| aa80d8cd0a | |||
| 77d197f14e | |||
| f98fbb778c | |||
| c46a0106f2 | |||
| cbf3db0be0 | |||
| 21f3710083 | |||
| 8ac5768f4f | |||
| 2458b9305c | |||
| a8909ea2a5 | |||
| ac7c35c6c2 | |||
| e4631b5a85 | |||
| e1c50b5dc5 | |||
| c6c5a5cd12 | |||
| bd4854a607 | |||
| cd0181e6f4 | |||
| 287b1d2b4d | |||
| 10c61bb0a7 | |||
| 92215ac34f | |||
| f64d50d8c8 | |||
| b74e0ce48f | |||
| 27cb0029a8 | |||
| ce6f193f06 | |||
| a862ffdde4 | |||
| 3f1cd8a118 | |||
| bb4b5838e3 | |||
| ea98d64184 | |||
| 98f3c56da5 | |||
| 20b7619380 | |||
| 7b1c3f05c7 | |||
| 9166998442 | |||
| e1f6b577bf | |||
| ba0d08b2a6 | |||
| e79c12a038 | |||
| 2ca5182a28 | |||
| 205e627209 | |||
| 425d4f3f63 | |||
| d69843e122 | |||
| d2586d3b59 | |||
| edab84c89b | |||
| dd08754f1f | |||
| 2cdfe85091 | |||
| a11acef36f | |||
| 1e34dbf616 | |||
| b3d4763ef6 | |||
| fe630e9383 | |||
| 826a20785f | |||
| 75932d7621 | |||
| 62d095af4f | |||
| 1594735aa0 | |||
| cbd0bdf9fc | |||
| d3e8e8fb9c | |||
| 66406c5a48 | |||
| 753c600dd2 | |||
| b28b1df348 | |||
| b94649162e | |||
| ee50e19dbd | |||
| cc23f8b831 | |||
| bac7b68bb1 | |||
| f9a622c89b | |||
| c321dc5e81 | |||
| 72f37c9df4 | |||
| 544eac0c8a | |||
| 823593ddae | |||
| 3600e704c4 | |||
| 0c79d756a4 | |||
| eb531a7a88 | |||
| d6634d30dc | |||
| f87806b1b4 | |||
| 2a5afeb5ff | |||
| fc5495f1ec | |||
| 699cc361a2 | |||
| 31bf4f10c0 | |||
| fe704af62f | |||
| e74517543d | |||
| 44acf19742 | |||
| bf20aa253e | |||
| 81c815840d | |||
| e9cd63dc5f | |||
| 1ae8f67d93 | |||
| daa1e10333 | |||
| a8a356e703 | |||
| ca440cc5dd | |||
| 95a9fb4f62 | |||
| 7db9e27112 | |||
| 03bcdbe3f7 | |||
| f0762a6213 | |||
| 67fbc6b3ad | |||
| d9662d7396 | |||
| 5ccbbf259d | |||
| 179c2f8723 | |||
| c76e0a40a7 | |||
| 03407e528f | |||
| 0c41d742cf | |||
| ed2f471a4e | |||
| 04efec101e | |||
| a6c69012cc | |||
| 0045c54d8e | |||
| 45436c006f | |||
| cc183c0da8 | |||
| 523f1df98b | |||
| 5843dff278 | |||
| 7f24f47978 | |||
| b1f9fd459e | |||
| 48988eb785 | |||
| 0045a885b9 | |||
| 0b57f60454 | |||
| f0857c7da2 | |||
| 15faa2e841 | |||
| da103f7197 | |||
| 1d3e42f92e | |||
| 20ced841dd | |||
| 54ebd0a796 | |||
| e636a7171b | |||
| e8f847065b | |||
| 1c806bb572 | |||
| 963133598f | |||
| fedaa74c47 | |||
| e322baf1d7 | |||
| 173a07cb59 | |||
| 364afff860 | |||
| 1b59e61b8e | |||
| b1f453f7ba | |||
| 175e842feb | |||
| d7a9a37a0e | |||
| 836b9240de | |||
| bdac2df4b9 | |||
| 57b507ad50 | |||
| 35201b69f6 | |||
| 0d138c26e9 | |||
| b4a7393dca | |||
| d86092df1a | |||
| b392d7f8e3 | |||
| 7cc7953879 | |||
| 7b26852a1f | |||
| f26b384697 | |||
| ab0531aa76 | |||
| 6873720d81 | |||
| 1e30c4a219 | |||
| 0a0e3ff970 | |||
| 5c42fd86a6 | |||
| 16cc829906 | |||
| 829e7cf33c | |||
| 02bfa90417 | |||
| 0b2466cf26 | |||
| 9d8df04c5c | |||
| 34a1697d50 | |||
| 17cf711c3d | |||
| ce0b19605a | |||
| 35bd9ecda3 | |||
| ca89849dd2 | |||
| ac1cb6d56b | |||
| dfbffea0fc | |||
| 7ae9c993f1 | |||
| 91d739f8d6 | |||
| f0c625d85c | |||
| b5f5e73076 | |||
| 1fb5eff7f1 | |||
| 5116cfd141 | |||
| e53a1f90b0 | |||
| 766c9628b0 | |||
| 6a4abdd74c | |||
| fc8bc5ba1e | |||
| 0fde5d44c0 | |||
| dc6b5a3d49 | |||
| 396522f249 | |||
| 86ab39e4ca | |||
| a4c9cb0e55 | |||
| e6c6feac10 | |||
| ca0aee58ab | |||
| 6391f2c43d | |||
| 32171bb70c | |||
| fd6675a3a3 | |||
| 9d883978a8 | |||
| 1aae65575c | |||
| c5d58e1aab | |||
| 56394471fa | |||
| 4cae6959df | |||
| f02d7b4516 | |||
| f5c83112df | |||
| a413dc81c1 | |||
| c9eddab191 | |||
| ec1268bd71 | |||
| 22eb2b5823 | |||
| 9669da026f | |||
| 7b14e54eab | |||
| 6b30ee4593 | |||
| 17c47a15da | |||
| 8f55517236 | |||
| 41ad086dfa | |||
| e19ef7dcae | |||
| f361265d70 | |||
| ef72e3ef77 | |||
| 770f1a1ca0 | |||
| e8fc91191f | |||
| 105ad3317d | |||
| 22bf4775cd | |||
| 5c6be7969a | |||
| c6e23f4be2 | |||
| 05547c22ec | |||
| cc7ac79fa6 | |||
| 4c5c27dfc1 | |||
| 4aabfbd52e | |||
| 6eab842361 | |||
| b729dfd702 | |||
| 6366840781 | |||
| 704a2ee90b | |||
| 484be9bfe6 | |||
| a99e070c26 | |||
| bf803f88af | |||
| 9af6febca5 | |||
| 0101d0a1bd | |||
| 266874609d | |||
| 2ba7feedfc | |||
| 43c67b4939 | |||
| 2d9915e43a | |||
| 2329b41bce | |||
| 536496184e | |||
| 429c32477c | |||
| f5d51b2061 | |||
| 2ad1aaa277 | |||
| 3afd32dbc1 | |||
| 092830ed07 | |||
| d118a6d3ff | |||
| fe97ffdc2f | |||
| 964d2ce59c | |||
| dc52684cbc | |||
| 191bedc56f | |||
| 47b2ace7fd | |||
| 9fb7359a3e | |||
| 4a5de26406 | |||
| 6fa18e126f | |||
| 1149002e0c | |||
| d704cb0b50 | |||
| d59e5ae9cf | |||
| 4587c1550d | |||
| b5bd329ada | |||
| beccd7a4ac | |||
| 61262fa939 | |||
| 7c6b006631 | |||
| dbd149354a | |||
| 4306ba5004 | |||
| 6de370b82f | |||
| 45781666b8 | |||
| 538231eb6f | |||
| eb74f87f2c | |||
| 59d71ffdcf | |||
| d1b93d4011 | |||
| d8ddf2e740 | |||
| 581327dc8e | |||
| 76e4512a0c | |||
| efdd55beca | |||
| 2c115649b9 | |||
| 2ddcc31a93 | |||
| 3bcce5b749 | |||
| 80dac27214 | |||
| 4a1177d048 | |||
| 4725d8f270 | |||
| 07b3528515 | |||
| d2d1b1ea26 | |||
| 232b897abc | |||
| efd076bc6c | |||
| cc877480ff | |||
| 363145a284 | |||
| 755571ad33 | |||
| 39edb55721 | |||
| 15aa7ecc2e | |||
| ce9e91153e | |||
| 9ee0a46606 | |||
| 20dc351f4c | |||
| c30c54d562 | |||
| 45ff51c0d2 | |||
| 5b86e99138 | |||
| 0c72910eb7 |
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Follow the troubleshooting guide before reporting a bug
|
||||||
|
|
||||||
|
---
|
||||||
|
**READ ME FIRST!**
|
||||||
|
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
|
||||||
|
|
||||||
|
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Steps to reproduce**
|
||||||
|
Any special steps that are required for the bug to appear.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||||
|
|
||||||
|
**Affected games**
|
||||||
|
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
|
||||||
|
|
||||||
|
**Other Moonlight clients**
|
||||||
|
- Does the issue occur when using Moonlight on PC or iOS?
|
||||||
|
|
||||||
|
**Moonlight settings (please complete the following information)**
|
||||||
|
- Have any settings been adjusted from defaults?
|
||||||
|
- If so, which settings have been changed?
|
||||||
|
- Does the problem still occur after reverting settings back to default?
|
||||||
|
|
||||||
|
**Gamepad-related issues (please complete if problem is gamepad-related)**
|
||||||
|
- Do you have any gamepads connected to your host PC directly?
|
||||||
|
- If gamepad input is not working, does it work if you use Moonlight's on-screen controls?
|
||||||
|
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
|
||||||
|
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
|
||||||
|
|
||||||
|
**Device details (please complete the following information)**
|
||||||
|
- Android version: [e.g. Android 10]
|
||||||
|
- Device model: [e.g. Samsung Galaxy S21]
|
||||||
|
|
||||||
|
**Server PC details (please complete the following information)**
|
||||||
|
- OS: [e.g. Windows 10 1809]
|
||||||
|
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||||
|
- Nvidia GPU driver: [e.g. 417.35]
|
||||||
|
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Anything else you think may be relevant to the issue or special about your specific setup.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
*.ap_
|
*.ap_
|
||||||
*.aab
|
*.aab
|
||||||
output.json
|
output.json
|
||||||
|
output-metadata.json
|
||||||
out/
|
out/
|
||||||
|
|
||||||
# files for the dex VM
|
# files for the dex VM
|
||||||
|
|||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
language: android
|
|
||||||
dist: trusty
|
|
||||||
|
|
||||||
git:
|
|
||||||
depth: 1
|
|
||||||
|
|
||||||
android:
|
|
||||||
components:
|
|
||||||
- tools
|
|
||||||
- platform-tools
|
|
||||||
- build-tools-29.0.3
|
|
||||||
- android-29
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- sdkmanager --list
|
|
||||||
|
|
||||||
install:
|
|
||||||
- yes | sdkmanager "ndk;20.0.5594570"
|
|
||||||
@@ -1,56 +1,28 @@
|
|||||||
# Moonlight Android
|
# Moonlight Android
|
||||||
|
|
||||||
[](https://travis-ci.org/moonlight-stream/moonlight-android)
|
[](https://ci.appveyor.com/project/cgutman/moonlight-android/branch/master)
|
||||||
|
[](https://hosted.weblate.org/projects/moonlight/moonlight-android/)
|
||||||
|
|
||||||
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight for Android](https://moonlight-stream.org) is an open source client for NVIDIA 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.
|
|
||||||
|
|
||||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
Moonlight for Android will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
whether in your own home or over the internet.
|
whether in your own home or over the internet.
|
||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [iOS/tvOS client](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
## Features
|
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-android/).
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
## Downloads
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* [Google Play Store](https://play.google.com/store/apps/details?id=com.limelight)
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* [Amazon App Store](https://www.amazon.com/gp/product/B00JK4MFN2)
|
||||||
|
* [F-Droid](https://f-droid.org/packages/com.limelight)
|
||||||
## Installation
|
* [APK](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||||
|
|
||||||
* Download and install Moonlight for Android from
|
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/com.limelight/), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
|
||||||
* Android device running 4.1 (Jelly Bean) or higher
|
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
|
||||||
* If you are connecting from outside the same network, turn on internet
|
|
||||||
streaming
|
|
||||||
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
|
||||||
* Accept the pairing confirmation on your PC and add the PIN if needed
|
|
||||||
* Tap your PC again to view the list of apps to stream
|
|
||||||
* Play games!
|
|
||||||
|
|
||||||
## Contribute
|
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
|
||||||
|
|
||||||
1. Fork us
|
|
||||||
2. Write code
|
|
||||||
3. Send Pull Requests
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
* Install Android Studio and the Android NDK
|
* Install Android Studio and the Android NDK
|
||||||
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
* 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.
|
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
||||||
* Build the APK using Android Studio
|
* Build the APK using Android Studio or gradle
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
|
|||||||
+15
-11
@@ -1,14 +1,16 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
ndkVersion "23.1.7779620"
|
||||||
|
|
||||||
|
compileSdkVersion 32
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 32
|
||||||
|
|
||||||
versionName "9.2"
|
versionName "10.2"
|
||||||
versionCode = 222
|
versionCode = 275
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "root"
|
flavorDimensions "root"
|
||||||
@@ -41,9 +43,9 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lint {
|
||||||
disable 'MissingTranslation'
|
disable 'MissingTranslation'
|
||||||
lintConfig file("lint.xml")
|
lintConfig file('lint.xml')
|
||||||
}
|
}
|
||||||
|
|
||||||
bundle {
|
bundle {
|
||||||
@@ -65,7 +67,6 @@ android {
|
|||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
|
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
useProguard false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
@@ -111,13 +112,16 @@ android {
|
|||||||
path "src/main/jni/Android.mk"
|
path "src/main/jni/Android.mk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate native debug symbols to allow Google Play to symbolicate our native crashes
|
||||||
|
android.defaultConfig.ndk.debugSymbolLevel = 'FULL'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.64'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.64'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
|
||||||
implementation 'org.jcodec:jcodec:0.2.3'
|
implementation 'org.jcodec:jcodec:0.2.3'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
|
implementation 'com.squareup.okhttp3:okhttp:3.12.13'
|
||||||
implementation 'com.squareup.okio:okio:1.17.5'
|
implementation 'com.squareup.okio:okio:1.17.5'
|
||||||
implementation 'org.jmdns:jmdns:3.5.5'
|
implementation 'org.jmdns:jmdns:3.5.7'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules_s"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:isGame="true"
|
android:isGame="true"
|
||||||
android:banner="@drawable/atv_banner"
|
android:banner="@drawable/atv_banner"
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:installLocation="auto"
|
android:installLocation="auto"
|
||||||
|
android:gwpAsanMode="always"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
<provider
|
<provider
|
||||||
android:name=".PosterContentProvider"
|
android:name=".PosterContentProvider"
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcView"
|
android:name=".PcView"
|
||||||
|
android:exported="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -96,6 +99,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".preferences.StreamSettings"
|
android:name=".preferences.StreamSettings"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
|
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:label="Streaming Settings">
|
android:label="Streaming Settings">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
@@ -105,6 +109,7 @@
|
|||||||
android:name=".preferences.AddComputerManually"
|
android:name=".preferences.AddComputerManually"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:windowSoftInputMode="stateVisible"
|
android:windowSoftInputMode="stateVisible"
|
||||||
|
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:label="Add Computer Manually">
|
android:label="Add Computer Manually">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
@@ -120,10 +125,20 @@
|
|||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:theme="@style/StreamTheme">
|
android:theme="@style/StreamTheme"
|
||||||
|
android:preferMinimalPostProcessing="true">
|
||||||
<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" />
|
||||||
|
|
||||||
|
<!-- Special metadata for NVIDIA Shield devices to prevent input buffering
|
||||||
|
and most importantly, opt out of mouse acceleration while streaming -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.nvidia.immediateInput"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.nvidia.rawCursorInput"
|
||||||
|
android:value="true" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.limelight;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
@@ -26,6 +27,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.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
@@ -59,17 +61,22 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private int lastRunningAppId;
|
private int lastRunningAppId;
|
||||||
private boolean suspendGridUpdates;
|
private boolean suspendGridUpdates;
|
||||||
private boolean inForeground;
|
private boolean inForeground;
|
||||||
|
private boolean showHiddenApps;
|
||||||
|
private HashSet<Integer> hiddenAppIds = new HashSet<>();
|
||||||
|
|
||||||
private final static int START_OR_RESUME_ID = 1;
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
private final static int CANCEL_ID = 3;
|
|
||||||
private final static int START_WITH_QUIT = 4;
|
private final static int START_WITH_QUIT = 4;
|
||||||
private final static int VIEW_DETAILS_ID = 5;
|
private final static int VIEW_DETAILS_ID = 5;
|
||||||
private final static int CREATE_SHORTCUT_ID = 6;
|
private final static int CREATE_SHORTCUT_ID = 6;
|
||||||
|
private final static int HIDE_APP_ID = 7;
|
||||||
|
|
||||||
|
public final static String HIDDEN_APPS_PREF_FILENAME = "HiddenApps";
|
||||||
|
|
||||||
public final static String NAME_EXTRA = "Name";
|
public final static String NAME_EXTRA = "Name";
|
||||||
public final static String UUID_EXTRA = "UUID";
|
public final static String UUID_EXTRA = "UUID";
|
||||||
public final static String NEW_PAIR_EXTRA = "NewPair";
|
public final static String NEW_PAIR_EXTRA = "NewPair";
|
||||||
|
public final static String SHOW_HIDDEN_APPS_EXTRA = "ShowHiddenApps";
|
||||||
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
@@ -98,13 +105,16 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
try {
|
try {
|
||||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
PreferenceConfiguration.readPreferences(AppView.this),
|
PreferenceConfiguration.readPreferences(AppView.this),
|
||||||
computer, localBinder.getUniqueId());
|
computer, localBinder.getUniqueId(),
|
||||||
|
showHiddenApps);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appGridAdapter.updateHiddenApps(hiddenAppIds, true);
|
||||||
|
|
||||||
// Now make the binder visible. We must do this after appGridAdapter
|
// Now make the binder visible. We must do this after appGridAdapter
|
||||||
// is set to prevent us from reaching updateUiWithServerinfo() and
|
// is set to prevent us from reaching updateUiWithServerinfo() and
|
||||||
// touching the appGridAdapter prior to initialization.
|
// touching the appGridAdapter prior to initialization.
|
||||||
@@ -285,8 +295,14 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
showHiddenApps = getIntent().getBooleanExtra(SHOW_HIDDEN_APPS_EXTRA, false);
|
||||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||||
|
|
||||||
|
SharedPreferences hiddenAppsPrefs = getSharedPreferences(HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE);
|
||||||
|
for (String hiddenAppIdStr : hiddenAppsPrefs.getStringSet(uuidString, new HashSet<String>())) {
|
||||||
|
hiddenAppIds.add(Integer.parseInt(hiddenAppIdStr));
|
||||||
|
}
|
||||||
|
|
||||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
|
||||||
TextView label = findViewById(R.id.appListText);
|
TextView label = findViewById(R.id.appListText);
|
||||||
@@ -298,6 +314,21 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
Service.BIND_AUTO_CREATE);
|
Service.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateHiddenApps(boolean hideImmediately) {
|
||||||
|
HashSet<String> hiddenAppIdStringSet = new HashSet<>();
|
||||||
|
|
||||||
|
for (Integer hiddenAppId : hiddenAppIds) {
|
||||||
|
hiddenAppIdStringSet.add(hiddenAppId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
getSharedPreferences(HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putStringSet(uuidString, hiddenAppIdStringSet)
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
appGridAdapter.updateHiddenApps(hiddenAppIds, hideImmediately);
|
||||||
|
}
|
||||||
|
|
||||||
private void populateAppGridWithCache() {
|
private void populateAppGridWithCache() {
|
||||||
try {
|
try {
|
||||||
// Try to load from cache
|
// Try to load from cache
|
||||||
@@ -355,9 +386,12 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@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);
|
||||||
|
|
||||||
|
menu.setHeaderTitle(selectedApp.app.getAppName());
|
||||||
|
|
||||||
if (lastRunningAppId != 0) {
|
if (lastRunningAppId != 0) {
|
||||||
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
||||||
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
@@ -365,10 +399,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||||
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
|
||||||
|
// Only show the hide checkbox if this is not the currently running app or it's already hidden
|
||||||
|
if (lastRunningAppId != selectedApp.app.getAppId() || selectedApp.isHidden) {
|
||||||
|
MenuItem hideAppItem = menu.add(Menu.NONE, HIDE_APP_ID, 3, getResources().getString(R.string.applist_menu_hide_app));
|
||||||
|
hideAppItem.setCheckable(true);
|
||||||
|
hideAppItem.setChecked(selectedApp.isHidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.add(Menu.NONE, VIEW_DETAILS_ID, 4, getResources().getString(R.string.applist_menu_details));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
// Only add an option to create shortcut if box art is loaded
|
// Only add an option to create shortcut if box art is loaded
|
||||||
@@ -379,7 +420,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
||||||
if (drawable != null && drawable.getBitmap() != null) {
|
if (drawable != null && drawable.getBitmap() != null) {
|
||||||
// We have a bitmap loaded too
|
// We have a bitmap loaded too
|
||||||
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 5, getResources().getString(R.string.applist_menu_scut));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,12 +471,20 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case CANCEL_ID:
|
case VIEW_DETAILS_ID:
|
||||||
|
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details), app.app.toString(), false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case VIEW_DETAILS_ID:
|
case HIDE_APP_ID:
|
||||||
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
|
if (item.isChecked()) {
|
||||||
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
|
// Transitioning hidden to shown
|
||||||
|
hiddenAppIds.remove(app.app.getAppId());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Transitioning shown to hidden
|
||||||
|
hiddenAppIds.add(app.app.getAppId());
|
||||||
|
}
|
||||||
|
updateHiddenApps(false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case CREATE_SHORTCUT_ID:
|
case CREATE_SHORTCUT_ID:
|
||||||
@@ -565,9 +614,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAdapterFragmentLayoutId() {
|
public int getAdapterFragmentLayoutId() {
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
return PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
||||||
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
R.layout.app_grid_view_small : R.layout.app_grid_view;
|
||||||
R.layout.app_grid_view_small : R.layout.app_grid_view);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -592,9 +640,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
listView.requestFocus();
|
listView.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppObject {
|
public static class AppObject {
|
||||||
public final NvApp app;
|
public final NvApp app;
|
||||||
public boolean isRunning;
|
public boolean isRunning;
|
||||||
|
public boolean isHidden;
|
||||||
|
|
||||||
public AppObject(NvApp app) {
|
public AppObject(NvApp app) {
|
||||||
if (app == null) {
|
if (app == null) {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ 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.InputCaptureManager;
|
||||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
import com.limelight.binding.input.TouchContext;
|
import com.limelight.binding.input.touch.AbsoluteTouchContext;
|
||||||
|
import com.limelight.binding.input.touch.RelativeTouchContext;
|
||||||
import com.limelight.binding.input.driver.UsbDriverService;
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
import com.limelight.binding.input.touch.TouchContext;
|
||||||
import com.limelight.binding.input.virtual_controller.VirtualController;
|
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||||
import com.limelight.binding.video.CrashListener;
|
import com.limelight.binding.video.CrashListener;
|
||||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||||
@@ -28,6 +30,7 @@ import com.limelight.ui.GameGestures;
|
|||||||
import com.limelight.ui.StreamView;
|
import com.limelight.ui.StreamView;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.NetHelper;
|
import com.limelight.utils.NetHelper;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.ShortcutHelper;
|
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;
|
||||||
@@ -61,6 +64,7 @@ import android.view.Display;
|
|||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
import android.view.Surface;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnGenericMotionListener;
|
import android.view.View.OnGenericMotionListener;
|
||||||
@@ -74,7 +78,6 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@@ -95,8 +98,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private static final int REFERENCE_HORIZ_RES = 1280;
|
private static final int REFERENCE_HORIZ_RES = 1280;
|
||||||
private static final int REFERENCE_VERT_RES = 720;
|
private static final int REFERENCE_VERT_RES = 720;
|
||||||
|
|
||||||
private static final int STYLUS_DEAD_ZONE_DELAY = 250;
|
private static final int STYLUS_DOWN_DEAD_ZONE_DELAY = 100;
|
||||||
private static final int STYLUS_DEAD_ZONE_RADIUS = 50;
|
private static final int STYLUS_DOWN_DEAD_ZONE_RADIUS = 20;
|
||||||
|
|
||||||
|
private static final int STYLUS_UP_DEAD_ZONE_DELAY = 150;
|
||||||
|
private static final int STYLUS_UP_DEAD_ZONE_RADIUS = 50;
|
||||||
|
|
||||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||||
|
|
||||||
@@ -111,6 +117,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean displayedFailureDialog = false;
|
private boolean displayedFailureDialog = false;
|
||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
private boolean autoEnterPip = false;
|
||||||
private boolean surfaceCreated = false;
|
private boolean surfaceCreated = false;
|
||||||
private boolean attemptedConnection = false;
|
private boolean attemptedConnection = false;
|
||||||
|
|
||||||
@@ -119,8 +126,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean grabbedInput = true;
|
private boolean grabbedInput = true;
|
||||||
private boolean grabComboDown = false;
|
private boolean grabComboDown = false;
|
||||||
private StreamView streamView;
|
private StreamView streamView;
|
||||||
private long stylusDownTime = 0;
|
private long lastAbsTouchUpTime = 0;
|
||||||
private float stylusDownX, stylusDownY;
|
private long lastAbsTouchDownTime = 0;
|
||||||
|
private float lastAbsTouchUpX, lastAbsTouchUpY;
|
||||||
|
private float lastAbsTouchDownX, lastAbsTouchDownY;
|
||||||
|
|
||||||
private boolean isHidingOverlays;
|
private boolean isHidingOverlays;
|
||||||
private TextView notificationOverlayView;
|
private TextView notificationOverlayView;
|
||||||
@@ -205,11 +214,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && prefConfig.stretchVideo) {
|
if (prefConfig.stretchVideo || shouldIgnoreInsetsForResolution(prefConfig.width, prefConfig.height)) {
|
||||||
// Allow the activity to layout under notches if the fill-screen option
|
// Allow the activity to layout under notches if the fill-screen option
|
||||||
// was turned on by the user
|
// was turned on by the user or it's a full-screen native resolution
|
||||||
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for events on the game surface
|
// Listen for events on the game surface
|
||||||
@@ -260,7 +275,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||||
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||||
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
|
boolean appSupportsHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
|
||||||
byte[] derCertData = Game.this.getIntent().getByteArrayExtra(EXTRA_SERVER_CERT);
|
byte[] derCertData = Game.this.getIntent().getByteArrayExtra(EXTRA_SERVER_CERT);
|
||||||
|
|
||||||
X509Certificate serverCert = null;
|
X509Certificate serverCert = null;
|
||||||
@@ -286,7 +301,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
shortcutHelper.reportComputerShortcutUsed(computer);
|
shortcutHelper.reportComputerShortcutUsed(computer);
|
||||||
if (appName != null) {
|
if (appName != null) {
|
||||||
// This may be null if launched from the "Resume Session" PC context menu item
|
// This may be null if launched from the "Resume Session" PC context menu item
|
||||||
shortcutHelper.reportGameLaunched(computer, new NvApp(appName, appId, willStreamHdr));
|
shortcutHelper.reportGameLaunched(computer, new NvApp(appName, appId, appSupportsHdr));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the MediaCodec helper before creating the decoder
|
// Initialize the MediaCodec helper before creating the decoder
|
||||||
@@ -294,42 +309,33 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
MediaCodecHelper.initialize(this, glPrefs.glRenderer);
|
MediaCodecHelper.initialize(this, glPrefs.glRenderer);
|
||||||
|
|
||||||
// Check if the user has enabled HDR
|
// Check if the user has enabled HDR
|
||||||
|
boolean willStreamHdr = false;
|
||||||
if (prefConfig.enableHdr) {
|
if (prefConfig.enableHdr) {
|
||||||
// Check if the app supports it
|
// Start our HDR checklist
|
||||||
if (!willStreamHdr) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
Toast.makeText(this, "This game does not support HDR10", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
// It does, so start our HDR checklist
|
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
// We already know the app supports HDR if willStreamHdr is set.
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
|
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
|
||||||
|
|
||||||
// We must now ensure our display is compatible with HDR10
|
// We must now ensure our display is compatible with HDR10
|
||||||
boolean foundHdr10 = false;
|
|
||||||
if (hdrCaps != null) {
|
if (hdrCaps != null) {
|
||||||
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||||
foundHdr10 = true;
|
willStreamHdr = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundHdr10) {
|
if (!willStreamHdr) {
|
||||||
// Nope, no HDR for us :(
|
// Nope, no HDR for us :(
|
||||||
willStreamHdr = false;
|
|
||||||
Toast.makeText(this, "Display does not support HDR10", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "Display does not support HDR10", Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Toast.makeText(this, "HDR requires Android 7.0 or later", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "HDR requires Android 7.0 or later", Toast.LENGTH_LONG).show();
|
||||||
willStreamHdr = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
willStreamHdr = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user has enabled performance stats overlay
|
// Check if the user has enabled performance stats overlay
|
||||||
if (prefConfig.enablePerfOverlay) {
|
if (prefConfig.enablePerfOverlay) {
|
||||||
@@ -362,9 +368,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
Toast.makeText(this, "Decoder does not support HEVC Main10HDR10", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "Decoder does not support HEVC Main10HDR10", Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 HEVC 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()) {
|
||||||
Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "No HEVC decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
|
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
|
||||||
@@ -383,52 +389,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
float displayRefreshRate = prepareDisplayForRendering();
|
float displayRefreshRate = prepareDisplayForRendering();
|
||||||
LimeLog.info("Display refresh rate: "+displayRefreshRate);
|
LimeLog.info("Display refresh rate: "+displayRefreshRate);
|
||||||
|
|
||||||
// HACK: Despite many efforts to ensure low latency consistent frame
|
// If the user requested frame pacing using a capped FPS, we will need to change our
|
||||||
// delivery, the best non-lossy mechanism is to buffer 1 extra frame
|
// desired FPS setting here in accordance with the active display refresh rate.
|
||||||
// in the output pipeline. Android does some buffering on its end
|
|
||||||
// in SurfaceFlinger and it's difficult (impossible?) to inspect
|
|
||||||
// the precise state of the buffer queue to the screen after we
|
|
||||||
// release a frame for rendering.
|
|
||||||
//
|
|
||||||
// Since buffering a frame adds latency and we are primarily a
|
|
||||||
// latency-optimized client, rather than one designed for picture-perfect
|
|
||||||
// accuracy, we will synthetically induce a negative pressure on the display
|
|
||||||
// output pipeline by driving the decoder input pipeline under the speed
|
|
||||||
// that the display can refresh. This ensures a constant negative pressure
|
|
||||||
// to keep latency down but does induce a periodic frame loss. However, this
|
|
||||||
// periodic frame loss is *way* less than what we'd already get in Marshmallow's
|
|
||||||
// display pipeline where frames are dropped outside of our control if they land
|
|
||||||
// on the same V-sync.
|
|
||||||
//
|
|
||||||
// Hopefully, we can get rid of this once someone comes up with a better way
|
|
||||||
// to track the state of the pipeline and time frames.
|
|
||||||
int roundedRefreshRate = Math.round(displayRefreshRate);
|
int roundedRefreshRate = Math.round(displayRefreshRate);
|
||||||
int chosenFrameRate = prefConfig.fps;
|
int chosenFrameRate = prefConfig.fps;
|
||||||
if (!prefConfig.disableFrameDrop || prefConfig.unlockFps) {
|
if (prefConfig.framePacing == PreferenceConfiguration.FRAME_PACING_CAP_FPS) {
|
||||||
if (Build.DEVICE.equals("coral") || Build.DEVICE.equals("flame")) {
|
if (prefConfig.fps >= roundedRefreshRate) {
|
||||||
// HACK: Pixel 4 (XL) ignores the preferred display mode and lowers refresh rate,
|
if (prefConfig.fps > roundedRefreshRate + 3) {
|
||||||
// causing frame pacing issues. See https://issuetracker.google.com/issues/143401475
|
|
||||||
// To work around this, use frame drop mode if we want to stream at >= 60 FPS.
|
|
||||||
if (prefConfig.fps >= 60) {
|
|
||||||
LimeLog.info("Using Pixel 4 rendering hack");
|
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (prefConfig.fps >= roundedRefreshRate) {
|
|
||||||
if (prefConfig.unlockFps) {
|
|
||||||
// Use frame drops when rendering above the screen frame rate
|
// Use frame drops when rendering above the screen frame rate
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
prefConfig.framePacing = PreferenceConfiguration.FRAME_PACING_BALANCED;
|
||||||
LimeLog.info("Using drop mode for FPS > Hz");
|
LimeLog.info("Using drop mode for FPS > Hz");
|
||||||
} else if (roundedRefreshRate <= 49) {
|
} else if (roundedRefreshRate <= 49) {
|
||||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
prefConfig.framePacing = PreferenceConfiguration.FRAME_PACING_BALANCED;
|
||||||
LimeLog.info("Bogus refresh rate: " + roundedRefreshRate);
|
LimeLog.info("Bogus refresh rate: " + roundedRefreshRate);
|
||||||
}
|
}
|
||||||
// HACK: Avoid crashing on some MTK devices
|
else {
|
||||||
else if (decoderRenderer.isBlacklistedForFrameRate(roundedRefreshRate - 1)) {
|
|
||||||
// Use the old rendering strategy on these broken devices
|
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
|
||||||
} else {
|
|
||||||
chosenFrameRate = roundedRefreshRate - 1;
|
chosenFrameRate = roundedRefreshRate - 1;
|
||||||
LimeLog.info("Adjusting FPS target for screen to " + chosenFrameRate);
|
LimeLog.info("Adjusting FPS target for screen to " + chosenFrameRate);
|
||||||
}
|
}
|
||||||
@@ -444,7 +420,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setLaunchRefreshRate(prefConfig.fps)
|
.setLaunchRefreshRate(prefConfig.fps)
|
||||||
.setRefreshRate(chosenFrameRate)
|
.setRefreshRate(chosenFrameRate)
|
||||||
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
|
.setApp(new NvApp(appName != null ? appName : "app", appId, appSupportsHdr))
|
||||||
.setBitrate(prefConfig.bitrate)
|
.setBitrate(prefConfig.bitrate)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
@@ -458,6 +434,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
.setAttachedGamepadMask(gamepadMask)
|
.setAttachedGamepadMask(gamepadMask)
|
||||||
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
|
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
|
||||||
.setAudioConfiguration(prefConfig.audioConfiguration)
|
.setAudioConfiguration(prefConfig.audioConfiguration)
|
||||||
|
.setAudioEncryption(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
@@ -469,9 +446,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// 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,
|
if (!prefConfig.touchscreenTrackpad) {
|
||||||
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
touchContextMap[i] = new AbsoluteTouchContext(conn, i, streamView);
|
||||||
streamView);
|
}
|
||||||
|
else {
|
||||||
|
touchContextMap[i] = new RelativeTouchContext(conn, i,
|
||||||
|
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
||||||
|
streamView, prefConfig);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use sustained performance mode on N+ to ensure consistent
|
// Use sustained performance mode on N+ to ensure consistent
|
||||||
@@ -550,23 +532,49 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
private PictureInPictureParams getPictureInPictureParams(boolean autoEnter) {
|
||||||
|
PictureInPictureParams.Builder builder =
|
||||||
|
new PictureInPictureParams.Builder()
|
||||||
|
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
|
||||||
|
.setSourceRectHint(new Rect(
|
||||||
|
streamView.getLeft(), streamView.getTop(),
|
||||||
|
streamView.getRight(), streamView.getBottom()));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
builder.setAutoEnterEnabled(autoEnter);
|
||||||
|
builder.setSeamlessResizeEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPipAutoEnter(boolean autoEnter) {
|
||||||
|
if (!prefConfig.enablePip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
setPictureInPictureParams(getPictureInPictureParams(autoEnter));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
autoEnterPip = autoEnter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUserLeaveHint() {
|
public void onUserLeaveHint() {
|
||||||
super.onUserLeaveHint();
|
super.onUserLeaveHint();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
// PiP is only supported on Oreo and later, and we don't need to manually enter PiP on
|
||||||
if (prefConfig.enablePip && connected) {
|
// Android S and later.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
|
if (autoEnterPip) {
|
||||||
try {
|
try {
|
||||||
// This has thrown all sorts of weird exceptions on Samsung devices
|
// This has thrown all sorts of weird exceptions on Samsung devices
|
||||||
// running Oreo. Just eat them and close gracefully on leave, rather
|
// running Oreo. Just eat them and close gracefully on leave, rather
|
||||||
// than crashing.
|
// than crashing.
|
||||||
enterPictureInPictureMode(
|
enterPictureInPictureMode(getPictureInPictureParams(false));
|
||||||
new PictureInPictureParams.Builder()
|
|
||||||
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
|
|
||||||
.setSourceRectHint(new Rect(
|
|
||||||
streamView.getLeft(), streamView.getTop(),
|
|
||||||
streamView.getRight(), streamView.getBottom()))
|
|
||||||
.build());
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
@@ -578,46 +586,43 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
public void onWindowFocusChanged(boolean hasFocus) {
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
super.onWindowFocusChanged(hasFocus);
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
// We can't guarantee the state of modifiers keys which may have
|
||||||
// Capture is lost when focus is lost, so it must be requested again
|
// lifted while focus was not on us. Clear the modifier state.
|
||||||
// when focus is regained.
|
this.modifierFlags = 0;
|
||||||
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
|
|
||||||
// Recapture the pointer if focus was regained. On Android Q,
|
// With Android native pointer capture, capture is lost when focus is lost,
|
||||||
// we have to delay a bit before requesting capture because otherwise
|
// so it must be requested again when focus is regained.
|
||||||
// we'll hit the "requestPointerCapture called for a window that has no focus"
|
inputCaptureProvider.onWindowFocusChanged(hasFocus);
|
||||||
// error and it will not actually capture the cursor.
|
|
||||||
Handler h = new Handler();
|
|
||||||
h.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
streamView.requestPointerCapture();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Remove when Android R SDK is finalized
|
private boolean isRefreshRateEqualMatch(float refreshRate) {
|
||||||
private static void setPreferMinimalPostProcessingWithReflection(WindowManager.LayoutParams windowLayoutParams, boolean isPreferred) {
|
return refreshRate >= prefConfig.fps &&
|
||||||
// Build.VERSION.PREVIEW_SDK_INT was added in M
|
refreshRate <= prefConfig.fps + 3;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
}
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && Build.VERSION.PREVIEW_SDK_INT == 0) {
|
|
||||||
// Don't attempt this reflection unless on Android R Developer Preview
|
private boolean isRefreshRateGoodMatch(float refreshRate) {
|
||||||
return;
|
return refreshRate >= prefConfig.fps &&
|
||||||
}
|
Math.round(refreshRate) % prefConfig.fps <= 3;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
return;
|
private boolean shouldIgnoreInsetsForResolution(int width, int height) {
|
||||||
|
// Never ignore insets for non-native resolutions
|
||||||
|
if (!PreferenceConfiguration.isNativeResolution(width, height)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Field field = windowLayoutParams.getClass().getDeclaredField("preferMinimalPostProcessing");
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
field.set(windowLayoutParams, isPreferred);
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
} catch (NoSuchFieldException e) {
|
// Ignore insets if this is an exact match for the display resolution
|
||||||
e.printStackTrace();
|
if ((width == candidate.getPhysicalWidth() && height == candidate.getPhysicalHeight()) ||
|
||||||
} catch (IllegalAccessException e) {
|
(height == candidate.getPhysicalWidth() && width == candidate.getPhysicalHeight())) {
|
||||||
e.printStackTrace();
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private float prepareDisplayForRendering() {
|
private float prepareDisplayForRendering() {
|
||||||
@@ -628,41 +633,81 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// On M, we can explicitly set the optimal display mode
|
// On M, we can explicitly set the optimal display mode
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Display.Mode bestMode = display.getMode();
|
Display.Mode bestMode = display.getMode();
|
||||||
|
boolean isNativeResolutionStream = PreferenceConfiguration.isNativeResolution(prefConfig.width, prefConfig.height);
|
||||||
|
boolean refreshRateIsGood = isRefreshRateGoodMatch(bestMode.getRefreshRate());
|
||||||
|
boolean refreshRateIsEqual = isRefreshRateEqualMatch(bestMode.getRefreshRate());
|
||||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate();
|
boolean refreshRateReduced = candidate.getRefreshRate() < bestMode.getRefreshRate();
|
||||||
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
boolean resolutionReduced = candidate.getPhysicalWidth() < bestMode.getPhysicalWidth() ||
|
||||||
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
candidate.getPhysicalHeight() < bestMode.getPhysicalHeight();
|
||||||
candidate.getPhysicalWidth() <= 4096;
|
boolean resolutionFitsStream = candidate.getPhysicalWidth() >= prefConfig.width &&
|
||||||
|
candidate.getPhysicalHeight() >= prefConfig.height;
|
||||||
|
|
||||||
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
|
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
|
||||||
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
|
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
|
||||||
|
|
||||||
// On non-4K streams, we force the resolution to never change
|
if (candidate.getPhysicalWidth() > 4096 && prefConfig.width <= 4096) {
|
||||||
if (prefConfig.width < 3840) {
|
// Avoid resolutions options above 4K to be safe
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On non-4K streams, we force the resolution to never change unless it's above
|
||||||
|
// 60 FPS, which may require a resolution reduction due to HDMI bandwidth limitations,
|
||||||
|
// or it's a native resolution stream.
|
||||||
|
if (prefConfig.width < 3840 && prefConfig.fps <= 60 && !isNativeResolutionStream) {
|
||||||
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
|
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
|
||||||
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
|
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
// Make sure the resolution doesn't regress unless if it's over 60 FPS
|
||||||
if (prefConfig.fps <= 60) {
|
// where we may need to reduce resolution to achieve the desired refresh rate.
|
||||||
if (candidate.getRefreshRate() >= 63) {
|
if (resolutionReduced && !(prefConfig.fps > 60 && resolutionFitsStream)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefConfig.framePacing != PreferenceConfiguration.FRAME_PACING_MIN_LATENCY &&
|
||||||
|
refreshRateIsEqual && !isRefreshRateEqualMatch(candidate.getRefreshRate())) {
|
||||||
|
// If we had an equal refresh rate and this one is not, skip it. In min latency
|
||||||
|
// mode, we want to always prefer the highest frame rate even though it may cause
|
||||||
|
// microstuttering.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (refreshRateIsGood) {
|
||||||
|
// We've already got a good match, so if this one isn't also good, it's not
|
||||||
|
// worth considering at all.
|
||||||
|
if (!isRefreshRateGoodMatch(candidate.getRefreshRate())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the refresh rate doesn't regress
|
// We don't want ever reduce our refresh rate unless we found an exact
|
||||||
if (!refreshRateOk) {
|
// match and we're not in min latency mode.
|
||||||
continue;
|
if (refreshRateReduced) {
|
||||||
|
if (prefConfig.framePacing == PreferenceConfiguration.FRAME_PACING_MIN_LATENCY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (!isRefreshRateEqualMatch(candidate.getRefreshRate())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else if (!isRefreshRateGoodMatch(candidate.getRefreshRate())) {
|
||||||
// Make sure the resolution doesn't regress
|
// We didn't have a good match and this match isn't good either, so just don't
|
||||||
if (!resolutionOk) {
|
// reduce the refresh rate.
|
||||||
continue;
|
if (refreshRateReduced) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We didn't have a good match and this match is good. Prefer this refresh rate
|
||||||
|
// even if it reduces the refresh rate. Lowering the refresh rate can be beneficial
|
||||||
|
// when streaming a 60 FPS stream on a 90 Hz device. We want to select 60 Hz to
|
||||||
|
// match the frame rate even if the active display mode is 90 Hz.
|
||||||
}
|
}
|
||||||
|
|
||||||
bestMode = candidate;
|
bestMode = candidate;
|
||||||
|
refreshRateIsGood = isRefreshRateGoodMatch(candidate.getRefreshRate());
|
||||||
|
refreshRateIsEqual = isRefreshRateEqualMatch(candidate.getRefreshRate());
|
||||||
}
|
}
|
||||||
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||||
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||||
@@ -673,9 +718,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
float bestRefreshRate = display.getRefreshRate();
|
float bestRefreshRate = display.getRefreshRate();
|
||||||
for (float candidate : display.getSupportedRefreshRates()) {
|
for (float candidate : display.getSupportedRefreshRates()) {
|
||||||
if (candidate > bestRefreshRate) {
|
LimeLog.info("Examining refresh rate: "+candidate);
|
||||||
LimeLog.info("Examining refresh rate: "+candidate);
|
|
||||||
|
|
||||||
|
if (candidate > bestRefreshRate) {
|
||||||
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
||||||
if (prefConfig.fps <= 60) {
|
if (prefConfig.fps <= 60) {
|
||||||
if (candidate >= 63) {
|
if (candidate >= 63) {
|
||||||
@@ -697,7 +742,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable HDMI ALLM (game mode) on Android R
|
// Enable HDMI ALLM (game mode) on Android R
|
||||||
setPreferMinimalPostProcessingWithReflection(windowLayoutParams, true);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
windowLayoutParams.preferMinimalPostProcessing = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the display mode change
|
// Apply the display mode change
|
||||||
getWindow().setAttributes(windowLayoutParams);
|
getWindow().setAttributes(windowLayoutParams);
|
||||||
@@ -741,9 +788,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return displayRefreshRate;
|
return displayRefreshRate;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Use the actual refresh rate of the display, since the preferred refresh rate or mode
|
// Use the lower of the current refresh rate and the selected refresh rate.
|
||||||
// may not actually be applied (ex: Pixel 4 with Smooth Display disabled).
|
// The preferred refresh rate may not actually be applied (ex: Battery Saver mode).
|
||||||
return getWindowManager().getDefaultDisplay().getRefreshRate();
|
return Math.min(getWindowManager().getDefaultDisplay().getRefreshRate(), displayRefreshRate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,6 +798,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private final Runnable hideSystemUi = new Runnable() {
|
private final Runnable hideSystemUi = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
// TODO: Do we want to use WindowInsetsController here on R+ instead of
|
||||||
|
// SYSTEM_UI_FLAG_IMMERSIVE_STICKY? They seem to do the same thing as of S...
|
||||||
|
|
||||||
// In multi-window mode on N+, we need to drop our layout flags or we'll
|
// In multi-window mode on N+, we need to drop our layout flags or we'll
|
||||||
// be drawing underneath the system UI.
|
// be drawing underneath the system UI.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode()) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode()) {
|
||||||
@@ -853,34 +903,36 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
stopConnection();
|
stopConnection();
|
||||||
|
|
||||||
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
if (prefConfig.enableLatencyToast) {
|
||||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||||
String message = null;
|
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||||
if (averageEndToEndLat > 0) {
|
String message = null;
|
||||||
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
if (averageEndToEndLat > 0) {
|
||||||
if (averageDecoderLat > 0) {
|
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
||||||
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
if (averageDecoderLat > 0) {
|
||||||
|
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (averageDecoderLat > 0) {
|
||||||
|
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (averageDecoderLat > 0) {
|
|
||||||
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the video codec to the post-stream toast
|
// Add the video codec to the post-stream toast
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
|
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
|
||||||
message += " [H.265 HDR]";
|
message += " [HEVC HDR]";
|
||||||
|
}
|
||||||
|
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
|
||||||
|
message += " [HEVC]";
|
||||||
|
}
|
||||||
|
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
|
||||||
|
message += " [H.264]";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
|
|
||||||
message += " [H.265]";
|
|
||||||
}
|
|
||||||
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
|
|
||||||
message += " [H.264]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the tombstone count if we terminated normally
|
// Clear the tombstone count if we terminated normally
|
||||||
@@ -970,8 +1022,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte getModifierState(KeyEvent event) {
|
// We cannot simply use modifierFlags for all key event processing, because
|
||||||
byte modifier = 0;
|
// some IMEs will not generate real key events for pressing Shift. Instead
|
||||||
|
// they will simply send key events with isShiftPressed() returning true,
|
||||||
|
// and we will need to send the modifier flag ourselves.
|
||||||
|
private byte getModifierState(KeyEvent event) {
|
||||||
|
// Start with the global modifier state to ensure we cover the case
|
||||||
|
// detailed in https://github.com/moonlight-stream/moonlight-android/issues/840
|
||||||
|
byte modifier = getModifierState();
|
||||||
if (event.isShiftPressed()) {
|
if (event.isShiftPressed()) {
|
||||||
modifier |= KeyboardPacket.MODIFIER_SHIFT;
|
modifier |= KeyboardPacket.MODIFIER_SHIFT;
|
||||||
}
|
}
|
||||||
@@ -1003,8 +1061,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Handle a synthetic back button event that some Android OS versions
|
// Handle a synthetic back button event that some Android OS versions
|
||||||
// create as a result of a right-click. This event WILL repeat if
|
// create as a result of a right-click. This event WILL repeat if
|
||||||
// the right mouse button is held down, so we ignore those.
|
// the right mouse button is held down, so we ignore those.
|
||||||
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
|
int eventSource = event.getSource();
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
if ((eventSource == InputDevice.SOURCE_MOUSE ||
|
||||||
|
eventSource == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
|
|
||||||
// Send the right mouse button event if mouse back and forward
|
// Send the right mouse button event if mouse back and forward
|
||||||
@@ -1052,7 +1111,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
byte modifiers = getModifierState(event);
|
byte modifiers = getModifierState(event);
|
||||||
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
|
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
|
||||||
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
|
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
|
||||||
conn.sendKeyboardInput((short) 0x8010, KeyboardPacket.KEY_DOWN, modifiers);
|
|
||||||
}
|
}
|
||||||
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, modifiers);
|
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, modifiers);
|
||||||
}
|
}
|
||||||
@@ -1074,8 +1132,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Handle a synthetic back button event that some Android OS versions
|
// Handle a synthetic back button event that some Android OS versions
|
||||||
// create as a result of a right-click.
|
// create as a result of a right-click.
|
||||||
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
|
int eventSource = event.getSource();
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
if ((eventSource == InputDevice.SOURCE_MOUSE ||
|
||||||
|
eventSource == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||||
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
|
|
||||||
// Send the right mouse button event if mouse back and forward
|
// Send the right mouse button event if mouse back and forward
|
||||||
@@ -1118,9 +1177,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
|
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
|
||||||
}
|
}
|
||||||
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, modifiers);
|
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, modifiers);
|
||||||
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
|
|
||||||
conn.sendKeyboardInput((short) 0x8010, KeyboardPacket.KEY_UP, getModifierState(event));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -1151,17 +1207,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
int eventSource = event.getSource();
|
||||||
|
if ((eventSource & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||||
if (controllerHandler.handleMotionEvent(event)) {
|
if (controllerHandler.handleMotionEvent(event)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
|
else if ((eventSource & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
|
(eventSource & InputDevice.SOURCE_CLASS_POSITION) != 0 ||
|
||||||
|
eventSource == InputDevice.SOURCE_MOUSE_RELATIVE)
|
||||||
{
|
{
|
||||||
// This case is for mice and non-finger touch devices
|
// This case is for mice and non-finger touch devices
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
if (eventSource == InputDevice.SOURCE_MOUSE ||
|
||||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
|
(eventSource & InputDevice.SOURCE_CLASS_POSITION) != 0 || // SOURCE_TOUCHPAD
|
||||||
|
eventSource == InputDevice.SOURCE_MOUSE_RELATIVE ||
|
||||||
(event.getPointerCount() >= 1 &&
|
(event.getPointerCount() >= 1 &&
|
||||||
(event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE ||
|
(event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE ||
|
||||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS ||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS ||
|
||||||
@@ -1176,10 +1235,57 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always update the position before sending any button events. If we're
|
||||||
|
// dealing with a stylus without hover support, our position might be
|
||||||
|
// significantly different than before.
|
||||||
|
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
||||||
|
// Send the deltas straight from the motion event
|
||||||
|
short deltaX = (short)inputCaptureProvider.getRelativeAxisX(event);
|
||||||
|
short deltaY = (short)inputCaptureProvider.getRelativeAxisY(event);
|
||||||
|
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
if (prefConfig.absoluteMouseMode) {
|
||||||
|
conn.sendMouseMoveAsMousePosition(deltaX, deltaY, (short)view.getWidth(), (short)view.getHeight());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseMove(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((eventSource & InputDevice.SOURCE_CLASS_POSITION) != 0) {
|
||||||
|
// If this input device is not associated with the view itself (like a trackpad),
|
||||||
|
// we'll convert the device-specific coordinates to use to send the cursor position.
|
||||||
|
// This really isn't ideal but it's probably better than nothing.
|
||||||
|
//
|
||||||
|
// Trackpad on newer versions of Android (Oreo and later) should be caught by the
|
||||||
|
// relative axes case above. If we get here, we're on an older version that doesn't
|
||||||
|
// support pointer capture.
|
||||||
|
InputDevice device = event.getDevice();
|
||||||
|
if (device != null) {
|
||||||
|
InputDevice.MotionRange xRange = device.getMotionRange(MotionEvent.AXIS_X, eventSource);
|
||||||
|
InputDevice.MotionRange yRange = device.getMotionRange(MotionEvent.AXIS_Y, eventSource);
|
||||||
|
|
||||||
|
// All touchpads coordinate planes should start at (0, 0)
|
||||||
|
if (xRange != null && yRange != null && xRange.getMin() == 0 && yRange.getMin() == 0) {
|
||||||
|
int xMax = (int)xRange.getMax();
|
||||||
|
int yMax = (int)yRange.getMax();
|
||||||
|
|
||||||
|
// Touchpads must be smaller than (65535, 65535)
|
||||||
|
if (xMax <= Short.MAX_VALUE && yMax <= Short.MAX_VALUE) {
|
||||||
|
conn.sendMousePosition((short)event.getX(), (short)event.getY(),
|
||||||
|
(short)xMax, (short)yMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (view != null) {
|
||||||
|
// Otherwise send absolute position based on the view for SOURCE_CLASS_POINTER
|
||||||
|
updateMousePosition(view, event);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
conn.sendMouseHighResScroll((short)(event.getAxisValue(MotionEvent.AXIS_VSCROLL) * 120));
|
||||||
conn.sendMouseScroll(vScrollClicks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
@@ -1235,16 +1341,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (event.getPointerCount() == 1 && event.getActionIndex() == 0) {
|
if (event.getPointerCount() == 1 && event.getActionIndex() == 0) {
|
||||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||||
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
|
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
|
||||||
stylusDownTime = SystemClock.uptimeMillis();
|
lastAbsTouchDownTime = SystemClock.uptimeMillis();
|
||||||
stylusDownX = event.getX(0);
|
lastAbsTouchDownX = event.getX(0);
|
||||||
stylusDownY = event.getY(0);
|
lastAbsTouchDownY = event.getY(0);
|
||||||
|
|
||||||
// Stylus is left click
|
// Stylus is left click
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||||
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
|
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
|
||||||
stylusDownTime = SystemClock.uptimeMillis();
|
lastAbsTouchDownTime = SystemClock.uptimeMillis();
|
||||||
stylusDownX = event.getX(0);
|
lastAbsTouchDownX = event.getX(0);
|
||||||
stylusDownY = event.getY(0);
|
lastAbsTouchDownY = event.getY(0);
|
||||||
|
|
||||||
// Eraser is right click
|
// Eraser is right click
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
@@ -1252,18 +1358,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
|
else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
|
||||||
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
|
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
|
||||||
// It looks odd to set these on ACTION_UP, but it makes sense.
|
lastAbsTouchUpTime = SystemClock.uptimeMillis();
|
||||||
// The last "down" position is actually when we come up.
|
lastAbsTouchUpX = event.getX(0);
|
||||||
stylusDownTime = SystemClock.uptimeMillis();
|
lastAbsTouchUpY = event.getY(0);
|
||||||
stylusDownX = event.getX(0);
|
|
||||||
stylusDownY = event.getY(0);
|
|
||||||
|
|
||||||
// Stylus is left click
|
// Stylus is left click
|
||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
|
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
|
||||||
stylusDownTime = SystemClock.uptimeMillis();
|
lastAbsTouchUpTime = SystemClock.uptimeMillis();
|
||||||
stylusDownX = event.getX(0);
|
lastAbsTouchUpX = event.getX(0);
|
||||||
stylusDownY = event.getY(0);
|
lastAbsTouchUpY = event.getY(0);
|
||||||
|
|
||||||
// Eraser is right click
|
// Eraser is right click
|
||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
@@ -1271,17 +1375,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get relative axis values if we can
|
|
||||||
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
|
||||||
// Send the deltas straight from the motion event
|
|
||||||
conn.sendMouseMove((short) inputCaptureProvider.getRelativeAxisX(event),
|
|
||||||
(short) inputCaptureProvider.getRelativeAxisY(event));
|
|
||||||
}
|
|
||||||
else if (view != null) {
|
|
||||||
// Otherwise send absolute position
|
|
||||||
updateMousePosition(view, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastButtonState = event.getButtonState();
|
lastButtonState = event.getButtonState();
|
||||||
}
|
}
|
||||||
// This case is for fingers
|
// This case is for fingers
|
||||||
@@ -1294,6 +1387,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (view == null && !prefConfig.touchscreenTrackpad) {
|
||||||
|
// Absolute touch events should be dropped outside our view.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int actionIndex = event.getActionIndex();
|
int actionIndex = event.getActionIndex();
|
||||||
|
|
||||||
int eventX = (int)event.getX(actionIndex);
|
int eventX = (int)event.getX(actionIndex);
|
||||||
@@ -1323,7 +1421,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
{
|
{
|
||||||
case MotionEvent.ACTION_POINTER_DOWN:
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
context.touchDownEvent(eventX, eventY);
|
for (TouchContext touchContext : touchContextMap) {
|
||||||
|
touchContext.setPointerCount(event.getPointerCount());
|
||||||
|
}
|
||||||
|
context.touchDownEvent(eventX, eventY, true);
|
||||||
break;
|
break;
|
||||||
case MotionEvent.ACTION_POINTER_UP:
|
case MotionEvent.ACTION_POINTER_UP:
|
||||||
case MotionEvent.ACTION_UP:
|
case MotionEvent.ACTION_UP:
|
||||||
@@ -1336,9 +1437,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.touchUpEvent(eventX, eventY);
|
context.touchUpEvent(eventX, eventY);
|
||||||
|
for (TouchContext touchContext : touchContextMap) {
|
||||||
|
touchContext.setPointerCount(event.getPointerCount() - 1);
|
||||||
|
}
|
||||||
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
|
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
|
||||||
// The original secondary touch now becomes primary
|
// The original secondary touch now becomes primary
|
||||||
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
|
context.touchDownEvent((int)event.getX(1), (int)event.getY(1), false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
@@ -1370,6 +1474,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
case MotionEvent.ACTION_CANCEL:
|
case MotionEvent.ACTION_CANCEL:
|
||||||
for (TouchContext aTouchContext : touchContextMap) {
|
for (TouchContext aTouchContext : touchContextMap) {
|
||||||
aTouchContext.cancelTouch();
|
aTouchContext.cancelTouch();
|
||||||
|
aTouchContext.setPointerCount(0);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -1403,13 +1508,29 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
float eventY = event.getY(0);
|
float eventY = event.getY(0);
|
||||||
|
|
||||||
if (event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
|
if (event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
|
||||||
(event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) ||
|
(event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER ||
|
||||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS)
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS))
|
||||||
{
|
{
|
||||||
if (SystemClock.uptimeMillis() - stylusDownTime <= STYLUS_DEAD_ZONE_DELAY &&
|
switch (event.getActionMasked()) {
|
||||||
Math.sqrt(Math.pow(eventX - stylusDownX, 2) + Math.pow(eventY - stylusDownY, 2)) <= STYLUS_DEAD_ZONE_RADIUS) {
|
case MotionEvent.ACTION_DOWN:
|
||||||
// Ignore small inputs shortly after the stylus has been pressed. This ensures users can reliably double click.
|
case MotionEvent.ACTION_HOVER_ENTER:
|
||||||
return;
|
case MotionEvent.ACTION_HOVER_EXIT:
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
if (SystemClock.uptimeMillis() - lastAbsTouchUpTime <= STYLUS_UP_DEAD_ZONE_DELAY &&
|
||||||
|
Math.sqrt(Math.pow(eventX - lastAbsTouchUpX, 2) + Math.pow(eventY - lastAbsTouchUpY, 2)) <= STYLUS_UP_DEAD_ZONE_RADIUS) {
|
||||||
|
// Enforce a small deadzone between touch up and hover or touch down to allow more precise double-clicking
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
if (SystemClock.uptimeMillis() - lastAbsTouchDownTime <= STYLUS_DOWN_DEAD_ZONE_DELAY &&
|
||||||
|
Math.sqrt(Math.pow(eventX - lastAbsTouchDownX, 2) + Math.pow(eventY - lastAbsTouchDownY, 2)) <= STYLUS_DOWN_DEAD_ZONE_RADIUS) {
|
||||||
|
// Enforce a small deadzone between touch down and move or touch up to allow more precise double-clicking
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1452,6 +1573,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
private void stopConnection() {
|
private void stopConnection() {
|
||||||
if (connecting || connected) {
|
if (connecting || connected) {
|
||||||
|
setPipAutoEnter(false);
|
||||||
connecting = connected = false;
|
connecting = connected = false;
|
||||||
|
|
||||||
controllerHandler.stop();
|
controllerHandler.stop();
|
||||||
@@ -1470,7 +1592,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stageFailed(final String stage, final int errorCode) {
|
public void stageFailed(final String stage, final int portFlags, final int errorCode) {
|
||||||
|
// Perform a connection test if the failure could be due to a blocked port
|
||||||
|
// This does network I/O, so don't do it on the main thread.
|
||||||
|
final int portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER, 443, portFlags);
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -1488,8 +1614,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
Toast.makeText(Game.this, getResources().getText(R.string.video_decoder_init_failed), Toast.LENGTH_LONG).show();
|
Toast.makeText(Game.this, getResources().getText(R.string.video_decoder_init_failed), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title),
|
String dialogText = getResources().getString(R.string.conn_error_msg) + " " + stage +" (error "+errorCode+")";
|
||||||
getResources().getString(R.string.conn_error_msg) + " " + stage +" (error "+errorCode+")", true);
|
|
||||||
|
if (portFlags != 0) {
|
||||||
|
dialogText += "\n\n" + getResources().getString(R.string.check_ports_msg) + "\n" +
|
||||||
|
MoonBridge.stringifyPortFlags(portFlags, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portTestResult != MoonBridge.ML_TEST_RESULT_INCONCLUSIVE && portTestResult != 0) {
|
||||||
|
dialogText += "\n\n" + getResources().getString(R.string.nettest_text_blocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title), dialogText, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1497,6 +1633,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionTerminated(final int errorCode) {
|
public void connectionTerminated(final int errorCode) {
|
||||||
|
// Perform a connection test if the failure could be due to a blocked port
|
||||||
|
// This does network I/O, so don't do it on the main thread.
|
||||||
|
final int portFlags = MoonBridge.getPortFlagsFromTerminationErrorCode(errorCode);
|
||||||
|
final int portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER,443, portFlags);
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -1513,9 +1654,41 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Display the error dialog if it was an unexpected termination.
|
// Display the error dialog if it was an unexpected termination.
|
||||||
// Otherwise, just finish the activity immediately.
|
// Otherwise, just finish the activity immediately.
|
||||||
if (errorCode != 0) {
|
if (errorCode != MoonBridge.ML_ERROR_GRACEFUL_TERMINATION) {
|
||||||
|
String message;
|
||||||
|
|
||||||
|
if (portTestResult != MoonBridge.ML_TEST_RESULT_INCONCLUSIVE && portTestResult != 0) {
|
||||||
|
// If we got a blocked result, that supersedes any other error message
|
||||||
|
message = getResources().getString(R.string.nettest_text_blocked);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch (errorCode) {
|
||||||
|
case MoonBridge.ML_ERROR_NO_VIDEO_TRAFFIC:
|
||||||
|
message = getResources().getString(R.string.no_video_received_error);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MoonBridge.ML_ERROR_NO_VIDEO_FRAME:
|
||||||
|
message = getResources().getString(R.string.no_frame_received_error);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MoonBridge.ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
|
||||||
|
case MoonBridge.ML_ERROR_PROTECTED_CONTENT:
|
||||||
|
message = getResources().getString(R.string.early_termination_error);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
message = getResources().getString(R.string.conn_terminated_msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portFlags != 0) {
|
||||||
|
message += "\n\n" + getResources().getString(R.string.check_ports_msg) + "\n" +
|
||||||
|
MoonBridge.stringifyPortFlags(portFlags, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
|
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
|
||||||
getResources().getString(R.string.conn_terminated_msg), true);
|
message, true);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
finish();
|
finish();
|
||||||
@@ -1565,6 +1738,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
spinner = null;
|
spinner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPipAutoEnter(true);
|
||||||
connected = true;
|
connected = true;
|
||||||
connecting = false;
|
connecting = false;
|
||||||
|
|
||||||
@@ -1618,6 +1792,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
controllerHandler.handleRumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
controllerHandler.handleRumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHdrMode(boolean enabled) {
|
||||||
|
LimeLog.info("Display HDR mode: " + (enabled ? "enabled" : "disabled"));
|
||||||
|
decoderRenderer.setHdrMode(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||||
if (!surfaceCreated) {
|
if (!surfaceCreated) {
|
||||||
@@ -1635,6 +1815,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
@Override
|
@Override
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
surfaceCreated = true;
|
surfaceCreated = true;
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
// Tell the OS about our frame rate to allow it to adapt the display refresh rate appropriately
|
||||||
|
holder.getSurface().setFrameRate(prefConfig.fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static int APP_LIST_ID = 1;
|
|
||||||
private final static int PAIR_ID = 2;
|
private final static int PAIR_ID = 2;
|
||||||
private final static int UNPAIR_ID = 3;
|
private final static int UNPAIR_ID = 3;
|
||||||
private final static int WOL_ID = 4;
|
private final static int WOL_ID = 4;
|
||||||
@@ -117,6 +116,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private final static int RESUME_ID = 6;
|
private final static int RESUME_ID = 6;
|
||||||
private final static int QUIT_ID = 7;
|
private final static int QUIT_ID = 7;
|
||||||
private final static int VIEW_DETAILS_ID = 8;
|
private final static int VIEW_DETAILS_ID = 8;
|
||||||
|
private final static int FULL_APP_LIST_ID = 9;
|
||||||
|
private final static int TEST_NETWORK_ID = 10;
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
@@ -154,6 +155,13 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Amazon review didn't like the help button because the wiki was not entirely
|
||||||
|
// navigable via the Fire TV remote (though the relevant parts were). Let's hide
|
||||||
|
// it on Fire TV.
|
||||||
|
if (getPackageManager().hasSystemFeature("amazon.hardware.fire_tv")) {
|
||||||
|
helpButton.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
getFragmentManager().beginTransaction()
|
getFragmentManager().beginTransaction()
|
||||||
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
@@ -316,15 +324,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
|
|
||||||
|
// Add a header with PC status details
|
||||||
|
menu.clearHeader();
|
||||||
|
String headerTitle = computer.details.name + " - ";
|
||||||
|
switch (computer.details.state)
|
||||||
|
{
|
||||||
|
case ONLINE:
|
||||||
|
headerTitle += getResources().getString(R.string.pcview_menu_header_online);
|
||||||
|
break;
|
||||||
|
case OFFLINE:
|
||||||
|
menu.setHeaderIcon(R.drawable.ic_pc_offline);
|
||||||
|
headerTitle += getResources().getString(R.string.pcview_menu_header_offline);
|
||||||
|
break;
|
||||||
|
case UNKNOWN:
|
||||||
|
headerTitle += getResources().getString(R.string.pcview_menu_header_unknown);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.setHeaderTitle(headerTitle);
|
||||||
|
|
||||||
// Inflate the context menu
|
// Inflate the context menu
|
||||||
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
||||||
computer.details.state == ComputerDetails.State.UNKNOWN) {
|
computer.details.state == ComputerDetails.State.UNKNOWN) {
|
||||||
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
}
|
||||||
else if (computer.details.pairState != PairState.PAIRED) {
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
|
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (computer.details.runningGameId != 0) {
|
if (computer.details.runningGameId != 0) {
|
||||||
@@ -332,13 +357,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
|
menu.add(Menu.NONE, FULL_APP_LIST_ID, 4, getResources().getString(R.string.pcview_menu_app_list));
|
||||||
|
|
||||||
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
|
|
||||||
// it with delete which actually work
|
|
||||||
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
|
||||||
}
|
}
|
||||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
|
|
||||||
|
menu.add(Menu.NONE, TEST_NETWORK_ID, 5, getResources().getString(R.string.pcview_menu_test_network));
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 6, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
|
menu.add(Menu.NONE, VIEW_DETAILS_ID, 7, getResources().getString(R.string.pcview_menu_details));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -350,8 +374,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
private void doPair(final ComputerDetails computer) {
|
||||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
if (computer.state == ComputerDetails.State.OFFLINE || computer.activeAddress == null) {
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -442,7 +465,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
if (toastSuccess) {
|
if (toastSuccess) {
|
||||||
// Open the app list after a successful pairing attempt
|
// Open the app list after a successful pairing attempt
|
||||||
doAppList(computer, true);
|
doAppList(computer, true, false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Start polling again if we're still in the foreground
|
// Start polling again if we're still in the foreground
|
||||||
@@ -488,8 +511,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doUnpair(final ComputerDetails computer) {
|
private void doUnpair(final ComputerDetails computer) {
|
||||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
if (computer.state == ComputerDetails.State.OFFLINE || computer.activeAddress == null) {
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -541,7 +563,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doAppList(ComputerDetails computer, boolean newlyPaired) {
|
private void doAppList(ComputerDetails computer, boolean newlyPaired, boolean showHiddenGames) {
|
||||||
if (computer.state == ComputerDetails.State.OFFLINE) {
|
if (computer.state == ComputerDetails.State.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
@@ -555,6 +577,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||||
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
|
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
|
||||||
|
i.putExtra(AppView.SHOW_HIDDEN_APPS_EXTRA, showHiddenGames);
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,8 +615,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case APP_LIST_ID:
|
case FULL_APP_LIST_ID:
|
||||||
doAppList(computer.details, false);
|
doAppList(computer.details, false, true);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case RESUME_ID:
|
case RESUME_ID:
|
||||||
@@ -625,6 +648,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case TEST_NETWORK_ID:
|
||||||
|
ServerHelper.doNetworkTest(PcView.this);
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -635,6 +662,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
|
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
|
||||||
|
|
||||||
|
// Delete hidden games preference value
|
||||||
|
getSharedPreferences(AppView.HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.remove(details.uuid)
|
||||||
|
.apply();
|
||||||
|
|
||||||
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
|
|
||||||
@@ -692,9 +725,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAdapterFragmentLayoutId() {
|
public int getAdapterFragmentLayoutId() {
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
return R.layout.pc_grid_view;
|
||||||
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
|
|
||||||
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -713,7 +744,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Pair an unpaired machine by default
|
// Pair an unpaired machine by default
|
||||||
doPair(computer.details);
|
doPair(computer.details);
|
||||||
} else {
|
} else {
|
||||||
doAppList(computer.details, false);
|
doAppList(computer.details, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -721,7 +752,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
registerForContextMenu(listView);
|
registerForContextMenu(listView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ComputerObject {
|
public static class ComputerObject {
|
||||||
public ComputerDetails details;
|
public ComputerDetails details;
|
||||||
|
|
||||||
public ComputerObject(ComputerDetails details) {
|
public ComputerObject(ComputerDetails details) {
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import com.limelight.computers.ComputerManagerService;
|
|||||||
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.PairingManager;
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
|
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.ServerHelper;
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ public class ShortcutTrampoline extends Activity {
|
|||||||
private NvApp app;
|
private NvApp app;
|
||||||
private ArrayList<Intent> intentStack = new ArrayList<>();
|
private ArrayList<Intent> intentStack = new ArrayList<>();
|
||||||
|
|
||||||
|
private int wakeHostTries = 10;
|
||||||
private ComputerDetails computer;
|
private ComputerDetails computer;
|
||||||
private SpinnerDialog blockingLoadSpinner;
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
|
|
||||||
@@ -79,6 +82,23 @@ public class ShortcutTrampoline extends Activity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to wake the target PC if it's offline (up to some retry limit)
|
||||||
|
if (details.state == ComputerDetails.State.OFFLINE && --wakeHostTries >= 0) {
|
||||||
|
try {
|
||||||
|
// Make a best effort attempt to wake the target PC
|
||||||
|
WakeOnLanSender.sendWolPacket(computer);
|
||||||
|
|
||||||
|
// If we sent at least one WoL packet, reset the computer state
|
||||||
|
// to force ComputerManager to poll it again.
|
||||||
|
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||||
|
return;
|
||||||
|
} catch (IOException e) {
|
||||||
|
// If we got an exception, we couldn't send a single WoL packet,
|
||||||
|
// so fallthrough into the offline error path.
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (details.state != ComputerDetails.State.UNKNOWN) {
|
if (details.state != ComputerDetails.State.UNKNOWN) {
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -102,9 +102,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
LimeLog.warning("Corrupted certificate");
|
LimeLog.warning("Corrupted certificate");
|
||||||
return false;
|
return false;
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
// Should never happen
|
throw new RuntimeException(e);
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (InvalidKeySpecException e) {
|
} catch (InvalidKeySpecException e) {
|
||||||
// May happen if the key is corrupt
|
// May happen if the key is corrupt
|
||||||
LimeLog.warning("Corrupted key");
|
LimeLog.warning("Corrupted key");
|
||||||
@@ -124,10 +122,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
||||||
keyPairGenerator.initialize(2048);
|
keyPairGenerator.initialize(2048);
|
||||||
keyPair = keyPairGenerator.generateKeyPair();
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
// Should never happen
|
throw new RuntimeException(e);
|
||||||
e1.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
@@ -152,8 +148,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
||||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Nothing should go wrong here
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
@@ -7,9 +8,11 @@ import android.hardware.usb.UsbDevice;
|
|||||||
import android.hardware.usb.UsbManager;
|
import android.hardware.usb.UsbManager;
|
||||||
import android.media.AudioAttributes;
|
import android.media.AudioAttributes;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.CombinedVibration;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.os.VibrationEffect;
|
import android.os.VibrationEffect;
|
||||||
import android.os.Vibrator;
|
import android.os.Vibrator;
|
||||||
|
import android.os.VibratorManager;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
@@ -69,9 +72,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
this.prefConfig = prefConfig;
|
this.prefConfig = prefConfig;
|
||||||
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
|
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
|
||||||
// HACK: For now we're hardcoding a 7% deadzone. Some deadzone
|
int deadzonePercentage = prefConfig.deadzonePercentage;
|
||||||
// is required for controller batching support to work.
|
|
||||||
int deadzonePercentage = 7;
|
|
||||||
|
|
||||||
int[] ids = InputDevice.getDeviceIds();
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
@@ -107,6 +108,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
|
defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
|
defaultContext.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||||
|
defaultContext.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||||
defaultContext.controllerNumber = (short) 0;
|
defaultContext.controllerNumber = (short) 0;
|
||||||
defaultContext.assignedControllerNumber = true;
|
defaultContext.assignedControllerNumber = true;
|
||||||
defaultContext.external = false;
|
defaultContext.external = false;
|
||||||
@@ -153,11 +156,36 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This can happen when gaining/losing input focus with some devices.
|
||||||
|
// Input devices that have a trackpad may gain/lose AXIS_RELATIVE_X/Y.
|
||||||
@Override
|
@Override
|
||||||
public void onInputDeviceChanged(int deviceId) {
|
public void onInputDeviceChanged(int deviceId) {
|
||||||
// Remove and re-add
|
InputDevice device = InputDevice.getDevice(deviceId);
|
||||||
onInputDeviceRemoved(deviceId);
|
if (device == null) {
|
||||||
onInputDeviceAdded(deviceId);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have a context for this device, we don't need to update anything
|
||||||
|
InputDeviceContext existingContext = inputDeviceContexts.get(deviceId);
|
||||||
|
if (existingContext == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Device changed: "+existingContext.name+" ("+deviceId+")");
|
||||||
|
|
||||||
|
// Don't release the controller number, because we will carry it over if it is present.
|
||||||
|
// We also want to make sure the change is invisible to the host PC to avoid an add/remove
|
||||||
|
// cycle for the gamepad which may break some games.
|
||||||
|
existingContext.destroy();
|
||||||
|
|
||||||
|
InputDeviceContext newContext = createInputDeviceContextForDevice(device);
|
||||||
|
|
||||||
|
// Copy over existing controller number state
|
||||||
|
newContext.assignedControllerNumber = existingContext.assignedControllerNumber;
|
||||||
|
newContext.reservedControllerNumber = existingContext.reservedControllerNumber;
|
||||||
|
newContext.controllerNumber = existingContext.controllerNumber;
|
||||||
|
|
||||||
|
inputDeviceContexts.put(deviceId, newContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
@@ -194,6 +222,28 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK for https://issuetracker.google.com/issues/163120692
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
if (device.getId() == -1) {
|
||||||
|
// This "virtual" device could be input from any of the attached devices.
|
||||||
|
// Look to see if any gamepads are connected.
|
||||||
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
|
for (int id : ids) {
|
||||||
|
InputDevice dev = InputDevice.getDevice(id);
|
||||||
|
if (dev == null) {
|
||||||
|
// This device was removed during enumeration
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are any gamepad devices connected, we'll
|
||||||
|
// report that this virtual device is a gamepad.
|
||||||
|
if (hasJoystickAxes(dev) || hasGamepadButtons(dev)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, we'll try anything that claims to be a non-alphabetic keyboard
|
// Otherwise, we'll try anything that claims to be a non-alphabetic keyboard
|
||||||
return device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC;
|
return device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC;
|
||||||
}
|
}
|
||||||
@@ -463,10 +513,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.productId = dev.getProductId();
|
context.productId = dev.getProductId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dev.getVibrator().hasVibrator()) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasDualAmplitudeControlledRumbleVibrators(dev.getVibratorManager())) {
|
||||||
|
context.vibratorManager = dev.getVibratorManager();
|
||||||
|
}
|
||||||
|
else if (dev.getVibrator().hasVibrator()) {
|
||||||
context.vibrator = dev.getVibrator();
|
context.vibrator = dev.getVibrator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect if the gamepad has Mode and Select buttons according to the Android key layouts.
|
||||||
|
// We do this first because other codepaths below may override these defaults.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
boolean[] buttons = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
|
||||||
|
context.hasMode = buttons[0];
|
||||||
|
context.hasSelect = buttons[1] || buttons[2];
|
||||||
|
}
|
||||||
|
|
||||||
context.leftStickXAxis = MotionEvent.AXIS_X;
|
context.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
|
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
|
||||||
@@ -525,6 +586,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// The old DS4 driver uses RX and RY for triggers
|
// The old DS4 driver uses RX and RY for triggers
|
||||||
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||||
|
|
||||||
|
// DS4 has Select and Mode buttons (possibly mapped non-standard)
|
||||||
|
context.hasSelect = true;
|
||||||
|
context.hasMode = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If it's not a non-standard DS4 controller, it's probably an Xbox controller or
|
// If it's not a non-standard DS4 controller, it's probably an Xbox controller or
|
||||||
@@ -606,6 +671,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.backIsStart = true;
|
context.backIsStart = true;
|
||||||
context.modeIsSelect = true;
|
context.modeIsSelect = true;
|
||||||
context.triggerDeadzone = 0.30f;
|
context.triggerDeadzone = 0.30f;
|
||||||
|
context.hasSelect = true;
|
||||||
|
context.hasMode = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,6 +690,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if (!hasStartKey[0] && !hasStartKey[1]) {
|
if (!hasStartKey[0] && !hasStartKey[1]) {
|
||||||
context.backIsStart = true;
|
context.backIsStart = true;
|
||||||
context.modeIsSelect = true;
|
context.modeIsSelect = true;
|
||||||
|
context.hasSelect = true;
|
||||||
|
context.hasMode = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,14 +700,23 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.triggerDeadzone = 0.30f;
|
context.triggerDeadzone = 0.30f;
|
||||||
}
|
}
|
||||||
// SHIELD controllers will use small stick deadzones
|
// SHIELD controllers will use small stick deadzones
|
||||||
else if (devName.contains("SHIELD")) {
|
else if (devName.contains("SHIELD") || devName.contains("NVIDIA Controller")) {
|
||||||
context.leftStickDeadzoneRadius = 0.07f;
|
// The big Nvidia button on the Shield controllers acts like a Search button. It
|
||||||
context.rightStickDeadzoneRadius = 0.07f;
|
// summons the Google Assistant on the Shield TV. On my Pixel 4, it seems to do
|
||||||
|
// nothing, so we can hijack it to act like a mode button.
|
||||||
|
if (devName.contains("NVIDIA Controller v01.03") || devName.contains("NVIDIA Controller v01.04")) {
|
||||||
|
context.searchIsMode = true;
|
||||||
|
context.hasMode = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// The Serval has a couple of unknown buttons that are start and select. It also has
|
// The Serval has a couple of unknown buttons that are start and select. It also has
|
||||||
// a back button which we want to ignore since there's already a select button.
|
// a back button which we want to ignore since there's already a select button.
|
||||||
else if (devName.contains("Razer Serval")) {
|
else if (devName.contains("Razer Serval")) {
|
||||||
context.isServal = true;
|
context.isServal = true;
|
||||||
|
|
||||||
|
// Serval has Select and Mode buttons (possibly mapped non-standard)
|
||||||
|
context.hasMode = true;
|
||||||
|
context.hasSelect = true;
|
||||||
}
|
}
|
||||||
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
|
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
|
||||||
// However, Microsoft released a firmware update with no change to VID/PID
|
// However, Microsoft released a firmware update with no change to VID/PID
|
||||||
@@ -649,6 +727,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
else if (devName.equals("Xbox Wireless Controller")) {
|
else if (devName.equals("Xbox Wireless Controller")) {
|
||||||
if (gasRange == null) {
|
if (gasRange == null) {
|
||||||
context.isNonStandardXboxBtController = true;
|
context.isNonStandardXboxBtController = true;
|
||||||
|
|
||||||
|
// Xbox One S has Select and Mode buttons (possibly mapped non-standard)
|
||||||
|
context.hasMode = true;
|
||||||
|
context.hasSelect = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -671,6 +753,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK for https://issuetracker.google.com/issues/163120692
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
if (event.getDeviceId() == -1) {
|
||||||
|
return defaultContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return the existing context if it exists
|
// Return the existing context if it exists
|
||||||
InputDeviceContext context = inputDeviceContexts.get(event.getDeviceId());
|
InputDeviceContext context = inputDeviceContexts.get(event.getDeviceId());
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
@@ -827,6 +916,46 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override mode button for 8BitDo controllers
|
||||||
|
if (context.vendorId == 0x2dc8 && event.getScanCode() == 306) {
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This mapping was adding in Android 10, then changed based on
|
||||||
|
// kernel changes (adding hid-nintendo) in Android 11. If we're
|
||||||
|
// on anything newer than Pie, just use the built-in mapping.
|
||||||
|
if ((context.vendorId == 0x057e && context.productId == 0x2009 && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) || // Switch Pro controller
|
||||||
|
(context.vendorId == 0x0f0d && context.productId == 0x00c1)) { // HORIPAD for Switch
|
||||||
|
switch (event.getScanCode()) {
|
||||||
|
case 0x130:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_A;
|
||||||
|
case 0x131:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_B;
|
||||||
|
case 0x132:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
|
case 0x133:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||||
|
case 0x134:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||||
|
case 0x135:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||||
|
case 0x136:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_L2;
|
||||||
|
case 0x137:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_R2;
|
||||||
|
case 0x138:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
|
case 0x139:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
case 0x13A:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||||
|
case 0x13B:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||||
|
case 0x13D:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (context.usesLinuxGamepadStandardFaceButtons) {
|
if (context.usesLinuxGamepadStandardFaceButtons) {
|
||||||
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
|
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
|
||||||
switch (event.getScanCode()) {
|
switch (event.getScanCode()) {
|
||||||
@@ -987,10 +1116,29 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// Emulate the select button with mode
|
// Emulate the select button with mode
|
||||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
}
|
}
|
||||||
|
else if (context.searchIsMode && keyCode == KeyEvent.KEYCODE_SEARCH) {
|
||||||
|
// Emulate the mode button with search
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
return keyCode;
|
return keyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int handleFlipFaceButtons(int keyCode) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_A:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_B;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_A;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_X:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
|
default:
|
||||||
|
return keyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Vector2d populateCachedVector(float x, float y) {
|
private Vector2d populateCachedVector(float x, float y) {
|
||||||
// Reinitialize our cached Vector2d object
|
// Reinitialize our cached Vector2d object
|
||||||
inputVector.initialize(x, y);
|
inputVector.initialize(x, y);
|
||||||
@@ -1161,7 +1309,58 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void rumbleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) {
|
@TargetApi(31)
|
||||||
|
private boolean hasDualAmplitudeControlledRumbleVibrators(VibratorManager vm) {
|
||||||
|
int[] vibratorIds = vm.getVibratorIds();
|
||||||
|
|
||||||
|
// There must be exactly 2 vibrators on this device
|
||||||
|
if (vibratorIds.length != 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both vibrators must have amplitude control
|
||||||
|
for (int vid : vibratorIds) {
|
||||||
|
if (!vm.getVibrator(vid).hasAmplitudeControl()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This must only be called if hasDualAmplitudeControlledRumbleVibrators() is true!
|
||||||
|
@TargetApi(31)
|
||||||
|
private void rumbleDualVibrators(VibratorManager vm, short lowFreqMotor, short highFreqMotor) {
|
||||||
|
// Normalize motor values to 0-255 amplitudes for VibrationManager
|
||||||
|
highFreqMotor = (short)((highFreqMotor >> 8) & 0xFF);
|
||||||
|
lowFreqMotor = (short)((lowFreqMotor >> 8) & 0xFF);
|
||||||
|
|
||||||
|
// If they're both zero, we can just call cancel().
|
||||||
|
if (lowFreqMotor == 0 && highFreqMotor == 0) {
|
||||||
|
vm.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's no documentation that states that vibrators for FF_RUMBLE input devices will
|
||||||
|
// always be enumerated in this order, but it seems consistent between Xbox Series X (USB),
|
||||||
|
// PS3 (USB), and PS4 (USB+BT) controllers on Android 12 Beta 3.
|
||||||
|
int[] vibratorIds = vm.getVibratorIds();
|
||||||
|
int[] vibratorAmplitudes = new int[] { highFreqMotor, lowFreqMotor };
|
||||||
|
|
||||||
|
CombinedVibration.ParallelCombination combo = CombinedVibration.startParallel();
|
||||||
|
|
||||||
|
for (int i = 0; i < vibratorIds.length; i++) {
|
||||||
|
// It's illegal to create a VibrationEffect with an amplitude of 0.
|
||||||
|
// Simply excluding that vibrator from our ParallelCombination will turn it off.
|
||||||
|
if (vibratorAmplitudes[i] != 0) {
|
||||||
|
combo.addVibrator(vibratorIds[i], VibrationEffect.createOneShot(60000, vibratorAmplitudes[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.vibrate(combo.combine());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rumbleSingleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) {
|
||||||
// Since we can only use a single amplitude value, compute the desired amplitude
|
// Since we can only use a single amplitude value, compute the desired amplitude
|
||||||
// by taking 80% of the big motor and 33% of the small motor, then capping to 255.
|
// by taking 80% of the big motor and 33% of the small motor, then capping to 255.
|
||||||
// NB: This value is now 0-255 as required by VibrationEffect.
|
// NB: This value is now 0-255 as required by VibrationEffect.
|
||||||
@@ -1217,9 +1416,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if (deviceContext.controllerNumber == controllerNumber) {
|
if (deviceContext.controllerNumber == controllerNumber) {
|
||||||
foundMatchingDevice = true;
|
foundMatchingDevice = true;
|
||||||
|
|
||||||
if (deviceContext.vibrator != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && deviceContext.vibratorManager != null) {
|
||||||
vibrated = true;
|
vibrated = true;
|
||||||
rumbleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor);
|
rumbleDualVibrators(deviceContext.vibratorManager, lowFreqMotor, highFreqMotor);
|
||||||
|
}
|
||||||
|
else if (deviceContext.vibrator != null) {
|
||||||
|
vibrated = true;
|
||||||
|
rumbleSingleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1239,12 +1442,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// controls that triggered the rumble. Vibrate the device if
|
// controls that triggered the rumble. Vibrate the device if
|
||||||
// the user has requested that behavior.
|
// the user has requested that behavior.
|
||||||
if (!foundMatchingDevice && prefConfig.onscreenController && !prefConfig.onlyL3R3 && prefConfig.vibrateOsc) {
|
if (!foundMatchingDevice && prefConfig.onscreenController && !prefConfig.onlyL3R3 && prefConfig.vibrateOsc) {
|
||||||
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
rumbleSingleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
||||||
}
|
}
|
||||||
else if (foundMatchingDevice && !vibrated && prefConfig.vibrateFallbackToDevice) {
|
else if (foundMatchingDevice && !vibrated && prefConfig.vibrateFallbackToDevice) {
|
||||||
// We found a device to vibrate but it didn't have rumble support. The user
|
// We found a device to vibrate but it didn't have rumble support. The user
|
||||||
// has requested us to vibrate the device in this case.
|
// has requested us to vibrate the device in this case.
|
||||||
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
rumbleSingleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1256,6 +1459,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
int keyCode = handleRemapping(context, event);
|
int keyCode = handleRemapping(context, event);
|
||||||
|
|
||||||
|
if (prefConfig.flipFaceButtons) {
|
||||||
|
keyCode = handleFlipFaceButtons(keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1269,7 +1477,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// UI thread.
|
// UI thread.
|
||||||
try {
|
try {
|
||||||
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
||||||
} catch (InterruptedException ignored) {}
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
@@ -1378,7 +1593,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||||
} catch (InterruptedException ignored) {}
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1396,7 +1618,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||||
} catch (InterruptedException ignored) {}
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1417,12 +1646,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
int keyCode = handleRemapping(context, event);
|
int keyCode = handleRemapping(context, event);
|
||||||
|
|
||||||
|
if (prefConfig.flipFaceButtons) {
|
||||||
|
keyCode = handleFlipFaceButtons(keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
|
context.hasMode = true;
|
||||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_START:
|
case KeyEvent.KEYCODE_BUTTON_START:
|
||||||
@@ -1434,6 +1669,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BACK:
|
case KeyEvent.KEYCODE_BACK:
|
||||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||||
|
context.hasSelect = true;
|
||||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
context.inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
@@ -1515,26 +1751,40 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start+LB acts like select for controllers with one button
|
// Start+LB acts like select for controllers with one button
|
||||||
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
|
if (!context.hasSelect) {
|
||||||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
|
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
|
||||||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
(context.inputMap == ControllerPacket.PLAY_FLAG &&
|
||||||
{
|
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||||
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
{
|
||||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||||
|
context.inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We detect select+start or start+RB as the special button combo
|
// If there is a physical select button, we'll use Start+Select as the special button combo
|
||||||
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG) ||
|
// otherwise we'll use Start+RB.
|
||||||
context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
|
if (!context.hasMode) {
|
||||||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
|
if (context.hasSelect) {
|
||||||
SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG)) {
|
||||||
{
|
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG);
|
||||||
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
|
||||||
|
|
||||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
|
||||||
|
(context.inputMap == ControllerPacket.PLAY_FLAG &&
|
||||||
|
SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||||
|
{
|
||||||
|
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||||
|
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't need to send repeat key down events, but the platform
|
// We don't need to send repeat key down events, but the platform
|
||||||
@@ -1618,7 +1868,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
usbDeviceContexts.put(controller.getControllerId(), context);
|
usbDeviceContexts.put(controller.getControllerId(), context);
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenericControllerContext {
|
static class GenericControllerContext {
|
||||||
public int id;
|
public int id;
|
||||||
public boolean external;
|
public boolean external;
|
||||||
|
|
||||||
@@ -1655,6 +1905,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
class InputDeviceContext extends GenericControllerContext {
|
class InputDeviceContext extends GenericControllerContext {
|
||||||
public String name;
|
public String name;
|
||||||
|
public VibratorManager vibratorManager;
|
||||||
public Vibrator vibrator;
|
public Vibrator vibrator;
|
||||||
|
|
||||||
public int leftStickXAxis = -1;
|
public int leftStickXAxis = -1;
|
||||||
@@ -1678,11 +1929,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public boolean isServal;
|
public boolean isServal;
|
||||||
public boolean backIsStart;
|
public boolean backIsStart;
|
||||||
public boolean modeIsSelect;
|
public boolean modeIsSelect;
|
||||||
|
public boolean searchIsMode;
|
||||||
public boolean ignoreBack;
|
public boolean ignoreBack;
|
||||||
public boolean hasJoystickAxes;
|
public boolean hasJoystickAxes;
|
||||||
public boolean pendingExit;
|
public boolean pendingExit;
|
||||||
|
|
||||||
public int emulatingButtonFlags = 0;
|
public int emulatingButtonFlags = 0;
|
||||||
|
public boolean hasSelect;
|
||||||
|
public boolean hasMode;
|
||||||
|
|
||||||
// Used for OUYA bumper state tracking since they force all buttons
|
// Used for OUYA bumper state tracking since they force all buttons
|
||||||
// up when the OUYA button goes down. We watch the last time we get
|
// up when the OUYA button goes down. We watch the last time we get
|
||||||
@@ -1698,7 +1952,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public void destroy() {
|
public void destroy() {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
|
||||||
if (vibrator != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && vibratorManager != null) {
|
||||||
|
vibratorManager.cancel();
|
||||||
|
}
|
||||||
|
else if (vibrator != null) {
|
||||||
vibrator.cancel();
|
vibrator.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-7
@@ -2,7 +2,9 @@ package com.limelight.binding.input.capture;
|
|||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.hardware.input.InputManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -12,11 +14,13 @@ import android.view.View;
|
|||||||
// pointer icon hiding behavior over our stream view just in case pointer capture
|
// pointer icon hiding behavior over our stream view just in case pointer capture
|
||||||
// is unavailable on this system (ex: DeX, ChromeOS)
|
// is unavailable on this system (ex: DeX, ChromeOS)
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider {
|
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider implements InputManager.InputDeviceListener {
|
||||||
|
private InputManager inputManager;
|
||||||
private View targetView;
|
private View targetView;
|
||||||
|
|
||||||
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
|
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
|
||||||
super(activity, targetView);
|
super(activity, targetView);
|
||||||
|
this.inputManager = activity.getSystemService(InputManager.class);
|
||||||
this.targetView = targetView;
|
this.targetView = targetView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,38 +28,133 @@ public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptu
|
|||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We only capture the pointer if we have a compatible InputDevice
|
||||||
|
// present. This is a workaround for an Android 12 regression causing
|
||||||
|
// incorrect mouse input when using the SPen.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-android/issues/1030
|
||||||
|
private boolean hasCaptureCompatibleInputDevice() {
|
||||||
|
for (int id : InputDevice.getDeviceIds()) {
|
||||||
|
InputDevice device = InputDevice.getDevice(id);
|
||||||
|
if (device == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip touchscreens when considering compatible capture devices.
|
||||||
|
// Samsung devices on Android 12 will report a sec_touchpad device
|
||||||
|
// with SOURCE_TOUCHSCREEN, SOURCE_KEYBOARD, and SOURCE_MOUSE.
|
||||||
|
// Upon enabling pointer capture, that device will switch to
|
||||||
|
// SOURCE_KEYBOARD and SOURCE_TOUCHPAD.
|
||||||
|
if (device.supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.supportsSource(InputDevice.SOURCE_MOUSE) ||
|
||||||
|
device.supportsSource(InputDevice.SOURCE_MOUSE_RELATIVE) ||
|
||||||
|
device.supportsSource(InputDevice.SOURCE_TOUCHPAD)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableCapture() {
|
public void enableCapture() {
|
||||||
super.enableCapture();
|
super.enableCapture();
|
||||||
targetView.requestPointerCapture();
|
|
||||||
|
// Listen for device events to enable/disable capture
|
||||||
|
inputManager.registerInputDeviceListener(this, null);
|
||||||
|
|
||||||
|
// Capture now if we have a capture-capable device
|
||||||
|
if (hasCaptureCompatibleInputDevice()) {
|
||||||
|
targetView.requestPointerCapture();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableCapture() {
|
public void disableCapture() {
|
||||||
super.disableCapture();
|
super.disableCapture();
|
||||||
|
inputManager.unregisterInputDeviceListener(this);
|
||||||
targetView.releasePointerCapture();
|
targetView.releasePointerCapture();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean focusActive) {
|
||||||
|
if (!focusActive || !isCapturing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recapture the pointer if focus was regained. On Android Q,
|
||||||
|
// we have to delay a bit before requesting capture because otherwise
|
||||||
|
// we'll hit the "requestPointerCapture called for a window that has no focus"
|
||||||
|
// error and it will not actually capture the cursor.
|
||||||
|
Handler h = new Handler();
|
||||||
|
h.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (hasCaptureCompatibleInputDevice()) {
|
||||||
|
targetView.requestPointerCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
// SOURCE_MOUSE_RELATIVE is how SOURCE_MOUSE appears when our view has pointer capture.
|
||||||
|
// SOURCE_TOUCHPAD will have relative axes populated iff our view has pointer capture.
|
||||||
|
// See https://developer.android.com/reference/android/view/View#requestPointerCapture()
|
||||||
|
int eventSource = event.getSource();
|
||||||
|
return (eventSource == InputDevice.SOURCE_MOUSE_RELATIVE && event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) ||
|
||||||
|
(eventSource == InputDevice.SOURCE_TOUCHPAD && targetView.hasPointerCapture());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
float x = event.getX();
|
int axis = (event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) ?
|
||||||
|
MotionEvent.AXIS_X : MotionEvent.AXIS_RELATIVE_X;
|
||||||
|
float x = event.getAxisValue(axis);
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
x += event.getHistoricalX(i);
|
x += event.getHistoricalAxisValue(axis, i);
|
||||||
}
|
}
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
float y = event.getY();
|
int axis = (event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) ?
|
||||||
|
MotionEvent.AXIS_Y : MotionEvent.AXIS_RELATIVE_Y;
|
||||||
|
float y = event.getAxisValue(axis);
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
y += event.getHistoricalY(i);
|
y += event.getHistoricalAxisValue(axis, i);
|
||||||
}
|
}
|
||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputDeviceAdded(int deviceId) {
|
||||||
|
// Check if we've added a capture-compatible device
|
||||||
|
if (!targetView.hasPointerCapture() && hasCaptureCompatibleInputDevice()) {
|
||||||
|
targetView.requestPointerCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputDeviceRemoved(int deviceId) {
|
||||||
|
// Check if the capture-compatible device was removed
|
||||||
|
if (targetView.hasPointerCapture() && !hasCaptureCompatibleInputDevice()) {
|
||||||
|
targetView.releasePointerCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputDeviceChanged(int deviceId) {
|
||||||
|
// Emulating a remove+add should be sufficient for our purposes.
|
||||||
|
//
|
||||||
|
// Note: This callback must be handled carefully because it can happen as a result of
|
||||||
|
// calling requestPointerCapture(). This can cause trackpad devices to gain SOURCE_MOUSE_RELATIVE
|
||||||
|
// and re-enter this callback.
|
||||||
|
onInputDeviceRemoved(deviceId);
|
||||||
|
onInputDeviceAdded(deviceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,6 @@ public abstract class InputCaptureProvider {
|
|||||||
public float getRelativeAxisY(MotionEvent event) {
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onWindowFocusChanged(boolean focusActive) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
// All mouse events should use relative axes, even if they are zero. This avoids triggering
|
||||||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
// cursor jumps if we get an event with no associated motion, like ACTION_DOWN or ACTION_UP.
|
||||||
|
return event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
|
||||||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ public abstract class AbstractXboxController extends AbstractController {
|
|||||||
// around when we call notifyDeviceAdded(), we won't be able to claim
|
// around when we call notifyDeviceAdded(), we won't be able to claim
|
||||||
// the controller number used by the original InputDevice.
|
// the controller number used by the original InputDevice.
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Report that we're added _before_ reporting input
|
// Report that we're added _before_ reporting input
|
||||||
notifyDeviceAdded();
|
notifyDeviceAdded();
|
||||||
|
|||||||
@@ -127,7 +127,12 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
// just returning a false result or returning 0 enumerated devices,
|
// just returning a false result or returning 0 enumerated devices,
|
||||||
// they throw an undocumented SecurityException from this call, crashing
|
// they throw an undocumented SecurityException from this call, crashing
|
||||||
// the whole app. :(
|
// the whole app. :(
|
||||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
int intentFlags = 0;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
// This PendingIntent must be mutable to allow the framework to populate EXTRA_DEVICE and EXTRA_PERMISSION_GRANTED.
|
||||||
|
intentFlags |= PendingIntent.FLAG_MUTABLE;
|
||||||
|
}
|
||||||
|
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), intentFlags));
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
|
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
0x0f0d, // Hori
|
0x0f0d, // Hori
|
||||||
0x1038, // SteelSeries
|
0x1038, // SteelSeries
|
||||||
0x11c9, // Nacon
|
0x11c9, // Nacon
|
||||||
|
0x1209, // Ardwiino
|
||||||
0x12ab, // Unknown
|
0x12ab, // Unknown
|
||||||
0x1430, // RedOctane
|
0x1430, // RedOctane
|
||||||
0x146b, // BigBen
|
0x146b, // BigBen
|
||||||
@@ -33,8 +34,11 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
0x15e4, // Numark
|
0x15e4, // Numark
|
||||||
0x162e, // Joytech
|
0x162e, // Joytech
|
||||||
0x1689, // Razer Onza
|
0x1689, // Razer Onza
|
||||||
|
0x1949, // Lab126 (Amazon Luna)
|
||||||
0x1bad, // Harmonix
|
0x1bad, // Harmonix
|
||||||
|
0x20d6, // PowerA
|
||||||
0x24c6, // PowerA
|
0x24c6, // PowerA
|
||||||
|
0x2f24, // GameSir
|
||||||
};
|
};
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
@@ -66,8 +70,8 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean handleRead(ByteBuffer buffer) {
|
protected boolean handleRead(ByteBuffer buffer) {
|
||||||
if (buffer.limit() < 14) {
|
if (buffer.remaining() < 14) {
|
||||||
LimeLog.severe("Read too small: "+buffer.limit());
|
LimeLog.severe("Read too small: "+buffer.remaining());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
0x0e6f, // Unknown
|
0x0e6f, // Unknown
|
||||||
0x0f0d, // Hori
|
0x0f0d, // Hori
|
||||||
0x1532, // Razer Wildcat
|
0x1532, // Razer Wildcat
|
||||||
|
0x20d6, // PowerA
|
||||||
0x24c6, // PowerA
|
0x24c6, // PowerA
|
||||||
|
0x2e24, // Hyperkin
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||||
|
private static final byte[] ONE_S_INIT = {0x05, 0x20, 0x00, 0x0f, 0x06};
|
||||||
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
|
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
|
||||||
0x00, 0x00, 0x00, (byte)0x80, 0x00};
|
0x00, 0x00, 0x00, (byte)0x80, 0x00};
|
||||||
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
|
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
|
||||||
@@ -38,6 +41,8 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
|
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
|
||||||
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
|
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
|
||||||
new InitPacket(0x0000, 0x0000, FW2015_INIT),
|
new InitPacket(0x0000, 0x0000, FW2015_INIT),
|
||||||
|
new InitPacket(0x045e, 0x02ea, ONE_S_INIT),
|
||||||
|
new InitPacket(0x045e, 0x0b00, ONE_S_INIT),
|
||||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
|
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
|
||||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
|
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
|
||||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
|
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
|
||||||
@@ -98,11 +103,21 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
switch (buffer.get())
|
switch (buffer.get())
|
||||||
{
|
{
|
||||||
case 0x20:
|
case 0x20:
|
||||||
|
if (buffer.remaining() < 17) {
|
||||||
|
LimeLog.severe("XBone button/axis read too small: "+buffer.remaining());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
buffer.position(buffer.position()+3);
|
buffer.position(buffer.position()+3);
|
||||||
processButtons(buffer);
|
processButtons(buffer);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case 0x07:
|
case 0x07:
|
||||||
|
if (buffer.remaining() < 4) {
|
||||||
|
LimeLog.severe("XBone mode read too small: "+buffer.remaining());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// The Xbox One S controller needs acks for mode reports otherwise
|
// The Xbox One S controller needs acks for mode reports otherwise
|
||||||
// it retransmits them forever.
|
// it retransmits them forever.
|
||||||
if (buffer.get() == 0x30) {
|
if (buffer.get() == 0x30) {
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
package com.limelight.binding.input.touch;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
public class AbsoluteTouchContext implements TouchContext {
|
||||||
|
private int lastTouchDownX = 0;
|
||||||
|
private int lastTouchDownY = 0;
|
||||||
|
private long lastTouchDownTime = 0;
|
||||||
|
private int lastTouchUpX = 0;
|
||||||
|
private int lastTouchUpY = 0;
|
||||||
|
private long lastTouchUpTime = 0;
|
||||||
|
private int lastTouchLocationX = 0;
|
||||||
|
private int lastTouchLocationY = 0;
|
||||||
|
private boolean cancelled;
|
||||||
|
private boolean confirmedLongPress;
|
||||||
|
private boolean confirmedTap;
|
||||||
|
private Timer longPressTimer;
|
||||||
|
private Timer tapDownTimer;
|
||||||
|
|
||||||
|
private final NvConnection conn;
|
||||||
|
private final int actionIndex;
|
||||||
|
private final View targetView;
|
||||||
|
|
||||||
|
private static final int SCROLL_SPEED_FACTOR = 3;
|
||||||
|
|
||||||
|
private static final int LONG_PRESS_TIME_THRESHOLD = 650;
|
||||||
|
private static final int LONG_PRESS_DISTANCE_THRESHOLD = 30;
|
||||||
|
|
||||||
|
private static final int DOUBLE_TAP_TIME_THRESHOLD = 250;
|
||||||
|
private static final int DOUBLE_TAP_DISTANCE_THRESHOLD = 60;
|
||||||
|
|
||||||
|
private static final int TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD = 100;
|
||||||
|
private static final int TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD = 20;
|
||||||
|
|
||||||
|
public AbsoluteTouchContext(NvConnection conn, int actionIndex, View view)
|
||||||
|
{
|
||||||
|
this.conn = conn;
|
||||||
|
this.actionIndex = actionIndex;
|
||||||
|
this.targetView = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getActionIndex()
|
||||||
|
{
|
||||||
|
return actionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
|
||||||
|
{
|
||||||
|
if (!isNewFinger) {
|
||||||
|
// We don't handle finger transitions for absolute mode
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchLocationX = lastTouchDownX = eventX;
|
||||||
|
lastTouchLocationY = lastTouchDownY = eventY;
|
||||||
|
lastTouchDownTime = SystemClock.uptimeMillis();
|
||||||
|
cancelled = confirmedTap = confirmedLongPress = false;
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
// Start the timers
|
||||||
|
startTapDownTimer();
|
||||||
|
startLongPressTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean distanceExceeds(int deltaX, int deltaY, double limit) {
|
||||||
|
return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) > limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePosition(int eventX, int eventY) {
|
||||||
|
// We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT.
|
||||||
|
// Normalize these to the view size. We can't just drop them because we won't always get an event
|
||||||
|
// right at the boundary of the view, so dropping them would result in our cursor never really
|
||||||
|
// reaching the sides of the screen.
|
||||||
|
eventX = Math.min(Math.max(eventX, 0), targetView.getWidth());
|
||||||
|
eventY = Math.min(Math.max(eventY, 0), targetView.getHeight());
|
||||||
|
|
||||||
|
conn.sendMousePosition((short)eventX, (short)eventY, (short)targetView.getWidth(), (short)targetView.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
// Cancel the timers
|
||||||
|
cancelLongPressTimer();
|
||||||
|
cancelTapDownTimer();
|
||||||
|
|
||||||
|
// Raise the mouse buttons that we currently have down
|
||||||
|
if (confirmedLongPress) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else if (confirmedTap) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we get here, this means that the tap completed within the touch down
|
||||||
|
// deadzone time. We'll need to send the touch down and up events now at the
|
||||||
|
// original touch down position.
|
||||||
|
tapConfirmed();
|
||||||
|
try {
|
||||||
|
// FIXME: Sleeping on the main thread sucks
|
||||||
|
Thread.sleep(50);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchLocationX = lastTouchUpX = eventX;
|
||||||
|
lastTouchLocationY = lastTouchUpY = eventY;
|
||||||
|
lastTouchUpTime = SystemClock.uptimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void startLongPressTimer() {
|
||||||
|
longPressTimer = new Timer(true);
|
||||||
|
longPressTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (AbsoluteTouchContext.this) {
|
||||||
|
// Check if someone cancelled us
|
||||||
|
if (longPressTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncancellable now
|
||||||
|
longPressTimer = null;
|
||||||
|
|
||||||
|
// This timer should have already expired, but cancel it just in case
|
||||||
|
cancelTapDownTimer();
|
||||||
|
|
||||||
|
// Switch from a left click to a right click after a long press
|
||||||
|
confirmedLongPress = true;
|
||||||
|
if (confirmedTap) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, LONG_PRESS_TIME_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void cancelLongPressTimer() {
|
||||||
|
if (longPressTimer != null) {
|
||||||
|
longPressTimer.cancel();
|
||||||
|
longPressTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void startTapDownTimer() {
|
||||||
|
tapDownTimer = new Timer(true);
|
||||||
|
tapDownTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (AbsoluteTouchContext.this) {
|
||||||
|
// Check if someone cancelled us
|
||||||
|
if (tapDownTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncancellable now
|
||||||
|
tapDownTimer = null;
|
||||||
|
|
||||||
|
// Start our tap
|
||||||
|
tapConfirmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void cancelTapDownTimer() {
|
||||||
|
if (tapDownTimer != null) {
|
||||||
|
tapDownTimer.cancel();
|
||||||
|
tapDownTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tapConfirmed() {
|
||||||
|
if (confirmedTap || confirmedLongPress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmedTap = true;
|
||||||
|
cancelTapDownTimer();
|
||||||
|
|
||||||
|
// Left button down at original position
|
||||||
|
if (lastTouchDownTime - lastTouchUpTime > DOUBLE_TAP_TIME_THRESHOLD ||
|
||||||
|
distanceExceeds(lastTouchDownX - lastTouchUpX, lastTouchDownY - lastTouchUpY, DOUBLE_TAP_DISTANCE_THRESHOLD)) {
|
||||||
|
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
||||||
|
updatePosition(lastTouchDownX, lastTouchDownY);
|
||||||
|
}
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (cancelled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
if (distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, LONG_PRESS_DISTANCE_THRESHOLD)) {
|
||||||
|
// Moved too far since touch down. Cancel the long press timer.
|
||||||
|
cancelLongPressTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore motion within the deadzone period after touch down
|
||||||
|
if (confirmedTap || distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD)) {
|
||||||
|
tapConfirmed();
|
||||||
|
updatePosition(eventX, eventY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (actionIndex == 1) {
|
||||||
|
conn.sendMouseHighResScroll((short)((eventY - lastTouchLocationY) * SCROLL_SPEED_FACTOR));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchLocationX = eventX;
|
||||||
|
lastTouchLocationY = eventY;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelTouch() {
|
||||||
|
cancelled = true;
|
||||||
|
|
||||||
|
// Cancel the timers
|
||||||
|
cancelLongPressTimer();
|
||||||
|
cancelTapDownTimer();
|
||||||
|
|
||||||
|
// Raise the mouse buttons
|
||||||
|
if (confirmedLongPress) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else if (confirmedTap) {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPointerCount(int pointerCount) {
|
||||||
|
if (actionIndex == 0 && pointerCount > 1) {
|
||||||
|
cancelTouch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
-20
@@ -1,14 +1,16 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input.touch;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class TouchContext {
|
public class RelativeTouchContext implements TouchContext {
|
||||||
private int lastTouchX = 0;
|
private int lastTouchX = 0;
|
||||||
private int lastTouchY = 0;
|
private int lastTouchY = 0;
|
||||||
private int originalTouchX = 0;
|
private int originalTouchX = 0;
|
||||||
@@ -17,31 +19,40 @@ public class TouchContext {
|
|||||||
private boolean cancelled;
|
private boolean cancelled;
|
||||||
private boolean confirmedMove;
|
private boolean confirmedMove;
|
||||||
private boolean confirmedDrag;
|
private boolean confirmedDrag;
|
||||||
|
private boolean confirmedScroll;
|
||||||
private Timer dragTimer;
|
private Timer dragTimer;
|
||||||
private double distanceMoved;
|
private double distanceMoved;
|
||||||
private double xFactor, yFactor;
|
private double xFactor, yFactor;
|
||||||
|
private int pointerCount;
|
||||||
|
private int maxPointerCountInGesture;
|
||||||
|
|
||||||
private final NvConnection conn;
|
private final NvConnection conn;
|
||||||
private final int actionIndex;
|
private final int actionIndex;
|
||||||
private final int referenceWidth;
|
private final int referenceWidth;
|
||||||
private final int referenceHeight;
|
private final int referenceHeight;
|
||||||
private final View targetView;
|
private final View targetView;
|
||||||
|
private final PreferenceConfiguration prefConfig;
|
||||||
|
|
||||||
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,
|
private static final int SCROLL_SPEED_FACTOR = 5;
|
||||||
int referenceWidth, int referenceHeight, View view)
|
|
||||||
|
public RelativeTouchContext(NvConnection conn, int actionIndex,
|
||||||
|
int referenceWidth, int referenceHeight,
|
||||||
|
View view, PreferenceConfiguration prefConfig)
|
||||||
{
|
{
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.actionIndex = actionIndex;
|
this.actionIndex = actionIndex;
|
||||||
this.referenceWidth = referenceWidth;
|
this.referenceWidth = referenceWidth;
|
||||||
this.referenceHeight = referenceHeight;
|
this.referenceHeight = referenceHeight;
|
||||||
this.targetView = view;
|
this.targetView = view;
|
||||||
|
this.prefConfig = prefConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getActionIndex()
|
public int getActionIndex()
|
||||||
{
|
{
|
||||||
return actionIndex;
|
return actionIndex;
|
||||||
@@ -57,8 +68,18 @@ public class TouchContext {
|
|||||||
|
|
||||||
private boolean isTap()
|
private boolean isTap()
|
||||||
{
|
{
|
||||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
if (confirmedDrag || confirmedMove || confirmedScroll) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this input wasn't the last finger down, do not report
|
||||||
|
// a tap. This ensures we don't report duplicate taps for each
|
||||||
|
// finger on a multi-finger tap gesture
|
||||||
|
if (actionIndex + 1 != maxPointerCountInGesture) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long timeDelta = SystemClock.uptimeMillis() - originalTouchTime;
|
||||||
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +93,8 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
@Override
|
||||||
|
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
|
||||||
{
|
{
|
||||||
// Get the view dimensions to scale inputs on this touch
|
// Get the view dimensions to scale inputs on this touch
|
||||||
xFactor = referenceWidth / (double)targetView.getWidth();
|
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||||
@@ -80,18 +102,23 @@ public class TouchContext {
|
|||||||
|
|
||||||
originalTouchX = lastTouchX = eventX;
|
originalTouchX = lastTouchX = eventX;
|
||||||
originalTouchY = lastTouchY = eventY;
|
originalTouchY = lastTouchY = eventY;
|
||||||
originalTouchTime = System.currentTimeMillis();
|
|
||||||
cancelled = confirmedDrag = confirmedMove = false;
|
|
||||||
distanceMoved = 0;
|
|
||||||
|
|
||||||
if (actionIndex == 0) {
|
if (isNewFinger) {
|
||||||
// Start the timer for engaging a drag
|
maxPointerCountInGesture = pointerCount;
|
||||||
startDragTimer();
|
originalTouchTime = SystemClock.uptimeMillis();
|
||||||
|
cancelled = confirmedDrag = confirmedMove = confirmedScroll = false;
|
||||||
|
distanceMoved = 0;
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
// Start the timer for engaging a drag
|
||||||
|
startDragTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void touchUpEvent(int eventX, int eventY)
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
@@ -116,7 +143,14 @@ public class TouchContext {
|
|||||||
// do input detection by polling
|
// do input detection by polling
|
||||||
try {
|
try {
|
||||||
Thread.sleep(100);
|
Thread.sleep(100);
|
||||||
} catch (InterruptedException ignored) {}
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
// Raise the mouse button
|
// Raise the mouse button
|
||||||
conn.sendMouseButtonUp(buttonIndex);
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
@@ -124,16 +158,24 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void startDragTimer() {
|
private synchronized void startDragTimer() {
|
||||||
|
// Cancel any existing drag timers
|
||||||
|
cancelDragTimer();
|
||||||
|
|
||||||
dragTimer = new Timer(true);
|
dragTimer = new Timer(true);
|
||||||
dragTimer.schedule(new TimerTask() {
|
dragTimer.schedule(new TimerTask() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
synchronized (TouchContext.this) {
|
synchronized (RelativeTouchContext.this) {
|
||||||
// Check if someone already set move
|
// Check if someone already set move
|
||||||
if (confirmedMove) {
|
if (confirmedMove) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The drag should only be processed for the primary finger
|
||||||
|
if (actionIndex != maxPointerCountInGesture - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if someone cancelled us
|
// Check if someone cancelled us
|
||||||
if (dragTimer == null) {
|
if (dragTimer == null) {
|
||||||
return;
|
return;
|
||||||
@@ -179,20 +221,33 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkForConfirmedScroll() {
|
||||||
|
// Enter scrolling mode if we've already left the tap zone
|
||||||
|
// and we have 2 fingers on screen. Leave scroll mode if
|
||||||
|
// we no longer have 2 fingers on screen
|
||||||
|
confirmedScroll = (actionIndex == 0 && pointerCount == 2 && confirmedMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean touchMoveEvent(int eventX, int eventY)
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
|
if (cancelled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||||
{
|
{
|
||||||
|
checkForConfirmedMove(eventX, eventY);
|
||||||
|
checkForConfirmedScroll();
|
||||||
|
|
||||||
// We only send moves and drags for the primary touch point
|
// We only send moves and drags for the primary touch point
|
||||||
if (actionIndex == 0) {
|
if (actionIndex == 0) {
|
||||||
checkForConfirmedMove(eventX, eventY);
|
|
||||||
|
|
||||||
int deltaX = eventX - lastTouchX;
|
int deltaX = eventX - lastTouchX;
|
||||||
int deltaY = eventY - lastTouchY;
|
int deltaY = eventY - lastTouchY;
|
||||||
|
|
||||||
// Scale the deltas based on the factors passed to our constructor
|
// Scale the deltas based on the factors passed to our constructor
|
||||||
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
|
deltaX = (int) Math.round((double) Math.abs(deltaX) * xFactor);
|
||||||
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
|
deltaY = (int) Math.round((double) Math.abs(deltaY) * yFactor);
|
||||||
|
|
||||||
// Fix up the signs
|
// Fix up the signs
|
||||||
if (eventX < lastTouchX) {
|
if (eventX < lastTouchX) {
|
||||||
@@ -202,6 +257,23 @@ public class TouchContext {
|
|||||||
deltaY = -deltaY;
|
deltaY = -deltaY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pointerCount == 2) {
|
||||||
|
if (confirmedScroll) {
|
||||||
|
conn.sendMouseHighResScroll((short)(deltaY * SCROLL_SPEED_FACTOR));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (prefConfig.absoluteMouseMode) {
|
||||||
|
conn.sendMouseMoveAsMousePosition(
|
||||||
|
(short) deltaX,
|
||||||
|
(short) deltaY,
|
||||||
|
(short) targetView.getWidth(),
|
||||||
|
(short) targetView.getHeight());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseMove((short) deltaX, (short) deltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
||||||
// non-zero to update lastTouch that way devices that report small touch events often
|
// non-zero to update lastTouch that way devices that report small touch events often
|
||||||
// will work correctly
|
// will work correctly
|
||||||
@@ -211,8 +283,6 @@ public class TouchContext {
|
|||||||
if (deltaY != 0) {
|
if (deltaY != 0) {
|
||||||
lastTouchY = eventY;
|
lastTouchY = eventY;
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lastTouchX = eventX;
|
lastTouchX = eventX;
|
||||||
@@ -223,6 +293,7 @@ public class TouchContext {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void cancelTouch() {
|
public void cancelTouch() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|
||||||
@@ -235,7 +306,17 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isCancelled() {
|
public boolean isCancelled() {
|
||||||
return cancelled;
|
return cancelled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPointerCount(int pointerCount) {
|
||||||
|
this.pointerCount = pointerCount;
|
||||||
|
|
||||||
|
if (pointerCount > maxPointerCountInGesture) {
|
||||||
|
maxPointerCountInGesture = pointerCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.limelight.binding.input.touch;
|
||||||
|
|
||||||
|
public interface TouchContext {
|
||||||
|
int getActionIndex();
|
||||||
|
void setPointerCount(int pointerCount);
|
||||||
|
boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger);
|
||||||
|
boolean touchMoveEvent(int eventX, int eventY);
|
||||||
|
void touchUpEvent(int eventX, int eventY);
|
||||||
|
void cancelTouch();
|
||||||
|
boolean isCancelled();
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import android.content.Context;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -210,7 +211,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
// calculate new radius sizes depending
|
// calculate new radius sizes depending
|
||||||
radius_complete = getPercent(getCorrectWidth() / 2, 100);
|
radius_complete = getPercent(getCorrectWidth() / 2, 100) - 2 * getDefaultStrokeWidth();
|
||||||
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||||
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||||
|
|
||||||
@@ -270,7 +271,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
||||||
// them to make precise movements.
|
// them to make precise movements.
|
||||||
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
||||||
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
SystemClock.uptimeMillis() - timeLastClick > timeoutDeadzone ||
|
||||||
movement_radius > radius_dead_zone) ?
|
movement_radius > radius_dead_zone) ?
|
||||||
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
|
|
||||||
@@ -311,7 +312,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
// check for double click
|
// check for double click
|
||||||
if (lastClickState == CLICK_STATE.SINGLE &&
|
if (lastClickState == CLICK_STATE.SINGLE &&
|
||||||
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
timeLastClick + timeoutDoubleClick > SystemClock.uptimeMillis()) {
|
||||||
click_state = CLICK_STATE.DOUBLE;
|
click_state = CLICK_STATE.DOUBLE;
|
||||||
notifyOnDoubleClick();
|
notifyOnDoubleClick();
|
||||||
} else {
|
} else {
|
||||||
@@ -319,7 +320,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
notifyOnClick();
|
notifyOnClick();
|
||||||
}
|
}
|
||||||
// reset last click timestamp
|
// reset last click timestamp
|
||||||
timeLastClick = System.currentTimeMillis();
|
timeLastClick = SystemClock.uptimeMillis();
|
||||||
// set item pressed and update
|
// set item pressed and update
|
||||||
setPressed(true);
|
setPressed(true);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -177,6 +177,15 @@ public class DigitalButton extends VirtualControllerElement {
|
|||||||
listener.onClick();
|
listener.onClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timerLongClick != null) {
|
||||||
|
timerLongClick.cancel();
|
||||||
|
timerLongClick = null;
|
||||||
|
}
|
||||||
|
if (longClickTimerTask != null) {
|
||||||
|
longClickTimerTask.cancel();
|
||||||
|
longClickTimerTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
timerLongClick = new Timer();
|
timerLongClick = new Timer();
|
||||||
longClickTimerTask = new TimerLongClickTimerTask();
|
longClickTimerTask = new TimerLongClickTimerTask();
|
||||||
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
||||||
@@ -200,9 +209,11 @@ public class DigitalButton extends VirtualControllerElement {
|
|||||||
// We may be called for a release without a prior click
|
// We may be called for a release without a prior click
|
||||||
if (timerLongClick != null) {
|
if (timerLongClick != null) {
|
||||||
timerLongClick.cancel();
|
timerLongClick.cancel();
|
||||||
|
timerLongClick = null;
|
||||||
}
|
}
|
||||||
if (longClickTimerTask != null) {
|
if (longClickTimerTask != null) {
|
||||||
longClickTimerTask.cancel();
|
longClickTimerTask.cancel();
|
||||||
|
longClickTimerTask = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-22
@@ -9,12 +9,11 @@ import android.util.DisplayMetrics;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.binding.input.ControllerHandler;
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,7 +21,7 @@ import java.util.Timer;
|
|||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class VirtualController {
|
public class VirtualController {
|
||||||
public class ControllerInputContext {
|
public static class ControllerInputContext {
|
||||||
public short inputMap = 0x0000;
|
public short inputMap = 0x0000;
|
||||||
public byte leftTrigger = 0x00;
|
public byte leftTrigger = 0x00;
|
||||||
public byte rightTrigger = 0x00;
|
public byte rightTrigger = 0x00;
|
||||||
@@ -40,11 +39,10 @@ public class VirtualController {
|
|||||||
|
|
||||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
private ControllerHandler controllerHandler;
|
private final ControllerHandler controllerHandler;
|
||||||
private Context context = null;
|
private final Context context;
|
||||||
|
|
||||||
private FrameLayout frame_layout = null;
|
private FrameLayout frame_layout = null;
|
||||||
private RelativeLayout relative_layout = null;
|
|
||||||
|
|
||||||
private Timer retransmitTimer;
|
private Timer retransmitTimer;
|
||||||
|
|
||||||
@@ -60,10 +58,6 @@ public class VirtualController {
|
|||||||
this.frame_layout = layout;
|
this.frame_layout = layout;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
relative_layout = new RelativeLayout(context);
|
|
||||||
|
|
||||||
frame_layout.addView(relative_layout);
|
|
||||||
|
|
||||||
buttonConfigure = new Button(context);
|
buttonConfigure = new Button(context);
|
||||||
buttonConfigure.setAlpha(0.25f);
|
buttonConfigure.setAlpha(0.25f);
|
||||||
buttonConfigure.setFocusable(false);
|
buttonConfigure.setFocusable(false);
|
||||||
@@ -87,7 +81,7 @@ public class VirtualController {
|
|||||||
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
relative_layout.invalidate();
|
buttonConfigure.invalidate();
|
||||||
|
|
||||||
for (VirtualControllerElement element : elements) {
|
for (VirtualControllerElement element : elements) {
|
||||||
element.invalidate();
|
element.invalidate();
|
||||||
@@ -99,11 +93,20 @@ public class VirtualController {
|
|||||||
|
|
||||||
public void hide() {
|
public void hide() {
|
||||||
retransmitTimer.cancel();
|
retransmitTimer.cancel();
|
||||||
relative_layout.setVisibility(View.INVISIBLE);
|
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
element.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonConfigure.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void show() {
|
public void show() {
|
||||||
relative_layout.setVisibility(View.VISIBLE);
|
for (VirtualControllerElement element : elements) {
|
||||||
|
element.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonConfigure.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
// HACK: GFE sometimes discards gamepad packets when they are received
|
// HACK: GFE sometimes discards gamepad packets when they are received
|
||||||
// very shortly after another. This can be critical if an axis zeroing packet
|
// very shortly after another. This can be critical if an axis zeroing packet
|
||||||
@@ -120,9 +123,11 @@ public class VirtualController {
|
|||||||
|
|
||||||
public void removeElements() {
|
public void removeElements() {
|
||||||
for (VirtualControllerElement element : elements) {
|
for (VirtualControllerElement element : elements) {
|
||||||
relative_layout.removeView(element);
|
frame_layout.removeView(element);
|
||||||
}
|
}
|
||||||
elements.clear();
|
elements.clear();
|
||||||
|
|
||||||
|
frame_layout.removeView(buttonConfigure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOpacity(int opacity) {
|
public void setOpacity(int opacity) {
|
||||||
@@ -134,10 +139,10 @@ public class VirtualController {
|
|||||||
|
|
||||||
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
||||||
elements.add(element);
|
elements.add(element);
|
||||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
|
||||||
layoutParams.setMargins(x, y, 0, 0);
|
layoutParams.setMargins(x, y, 0, 0);
|
||||||
|
|
||||||
relative_layout.addView(element, layoutParams);
|
frame_layout.addView(element, layoutParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<VirtualControllerElement> getElements() {
|
public List<VirtualControllerElement> getElements() {
|
||||||
@@ -146,23 +151,20 @@ public class VirtualController {
|
|||||||
|
|
||||||
private static final void _DBG(String text) {
|
private static final void _DBG(String text) {
|
||||||
if (_PRINT_DEBUG_INFORMATION) {
|
if (_PRINT_DEBUG_INFORMATION) {
|
||||||
System.out.println("VirtualController: " + text);
|
LimeLog.info("VirtualController: " + text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshLayout() {
|
public void refreshLayout() {
|
||||||
relative_layout.removeAllViews();
|
|
||||||
removeElements();
|
removeElements();
|
||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
int buttonSize = (int)(screen.heightPixels*0.06f);
|
int buttonSize = (int)(screen.heightPixels*0.06f);
|
||||||
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(buttonSize, buttonSize);
|
||||||
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
|
|
||||||
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
|
|
||||||
params.leftMargin = 15;
|
params.leftMargin = 15;
|
||||||
params.topMargin = 15;
|
params.topMargin = 15;
|
||||||
relative_layout.addView(buttonConfigure, params);
|
frame_layout.addView(buttonConfigure, params);
|
||||||
|
|
||||||
// Start with the default layout
|
// Start with the default layout
|
||||||
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
||||||
|
|||||||
+12
-8
@@ -167,11 +167,11 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
private static final int DPAD_BASE_Y = 41;
|
private static final int DPAD_BASE_Y = 41;
|
||||||
private static final int DPAD_SIZE = 30;
|
private static final int DPAD_SIZE = 30;
|
||||||
|
|
||||||
private static final int ANALOG_L_BASE_X = 4;
|
private static final int ANALOG_L_BASE_X = 6;
|
||||||
private static final int ANALOG_L_BASE_Y = 1;
|
private static final int ANALOG_L_BASE_Y = 4;
|
||||||
private static final int ANALOG_R_BASE_X = 96;
|
private static final int ANALOG_R_BASE_X = 98;
|
||||||
private static final int ANALOG_R_BASE_Y = 42;
|
private static final int ANALOG_R_BASE_Y = 42;
|
||||||
private static final int ANALOG_SIZE = 28;
|
private static final int ANALOG_SIZE = 26;
|
||||||
|
|
||||||
private static final int L3_R3_BASE_Y = 60;
|
private static final int L3_R3_BASE_Y = 60;
|
||||||
|
|
||||||
@@ -205,7 +205,8 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_A,
|
VirtualControllerElement.EID_A,
|
||||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
!config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
|
||||||
|
!config.flipFaceButtons ? "A" : "B", -1, controller, context),
|
||||||
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
||||||
screenScale(BUTTON_BASE_Y + 2 * BUTTON_SIZE, height),
|
screenScale(BUTTON_BASE_Y + 2 * BUTTON_SIZE, height),
|
||||||
screenScale(BUTTON_SIZE, height),
|
screenScale(BUTTON_SIZE, height),
|
||||||
@@ -214,7 +215,8 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_B,
|
VirtualControllerElement.EID_B,
|
||||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
|
||||||
|
config.flipFaceButtons ? "A" : "B", -1, controller, context),
|
||||||
screenScale(BUTTON_BASE_X + BUTTON_SIZE, height) + rightDisplacement,
|
screenScale(BUTTON_BASE_X + BUTTON_SIZE, height) + rightDisplacement,
|
||||||
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
||||||
screenScale(BUTTON_SIZE, height),
|
screenScale(BUTTON_SIZE, height),
|
||||||
@@ -223,7 +225,8 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_X,
|
VirtualControllerElement.EID_X,
|
||||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
!config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
|
||||||
|
!config.flipFaceButtons ? "X" : "Y", -1, controller, context),
|
||||||
screenScale(BUTTON_BASE_X - BUTTON_SIZE, height) + rightDisplacement,
|
screenScale(BUTTON_BASE_X - BUTTON_SIZE, height) + rightDisplacement,
|
||||||
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
||||||
screenScale(BUTTON_SIZE, height),
|
screenScale(BUTTON_SIZE, height),
|
||||||
@@ -232,7 +235,8 @@ public class VirtualControllerConfigurationLoader {
|
|||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
controller.addElement(createDigitalButton(
|
||||||
VirtualControllerElement.EID_Y,
|
VirtualControllerElement.EID_Y,
|
||||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
|
||||||
|
config.flipFaceButtons ? "X" : "Y", -1, controller, context),
|
||||||
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
||||||
screenScale(BUTTON_BASE_Y, height),
|
screenScale(BUTTON_BASE_Y, height),
|
||||||
screenScale(BUTTON_SIZE, height),
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
|||||||
+5
-5
@@ -12,7 +12,7 @@ import android.graphics.Paint;
|
|||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@@ -72,7 +72,7 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
int newPos_x = (int) getX() + x - pressed_x;
|
int newPos_x = (int) getX() + x - pressed_x;
|
||||||
int newPos_y = (int) getY() + y - pressed_y;
|
int newPos_y = (int) getY() + y - pressed_y;
|
||||||
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
||||||
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
||||||
@@ -83,7 +83,7 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
int newHeight = height + (startSize_y - pressed_y);
|
int newHeight = height + (startSize_y - pressed_y);
|
||||||
int newWidth = width + (startSize_x - pressed_x);
|
int newWidth = width + (startSize_x - pressed_x);
|
||||||
@@ -316,7 +316,7 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
public JSONObject getConfiguration() throws JSONException {
|
public JSONObject getConfiguration() throws JSONException {
|
||||||
JSONObject configuration = new JSONObject();
|
JSONObject configuration = new JSONObject();
|
||||||
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
configuration.put("LEFT", layoutParams.leftMargin);
|
configuration.put("LEFT", layoutParams.leftMargin);
|
||||||
configuration.put("TOP", layoutParams.topMargin);
|
configuration.put("TOP", layoutParams.topMargin);
|
||||||
@@ -327,7 +327,7 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void loadConfiguration(JSONObject configuration) throws JSONException {
|
public void loadConfiguration(JSONObject configuration) throws JSONException {
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
layoutParams.leftMargin = configuration.getInt("LEFT");
|
layoutParams.leftMargin = configuration.getInt("LEFT");
|
||||||
layoutParams.topMargin = configuration.getInt("TOP");
|
layoutParams.topMargin = configuration.getInt("TOP");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.limelight.binding.video;
|
|||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import org.jcodec.codecs.h264.H264Utils;
|
import org.jcodec.codecs.h264.H264Utils;
|
||||||
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
||||||
@@ -20,10 +21,14 @@ import android.media.MediaFormat;
|
|||||||
import android.media.MediaCodec.BufferInfo;
|
import android.media.MediaCodec.BufferInfo;
|
||||||
import android.media.MediaCodec.CodecException;
|
import android.media.MediaCodec.CodecException;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.util.Range;
|
import android.util.Range;
|
||||||
|
import android.view.Choreographer;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements Choreographer.FrameCallback {
|
||||||
|
|
||||||
private static final boolean USE_FRAME_RENDER_TIME = false;
|
private static final boolean USE_FRAME_RENDER_TIME = false;
|
||||||
private static final boolean FRAME_RENDER_TIME_ONLY = USE_FRAME_RENDER_TIME && false;
|
private static final boolean FRAME_RENDER_TIME_ONLY = USE_FRAME_RENDER_TIME && false;
|
||||||
@@ -44,8 +49,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private boolean adaptivePlayback, directSubmit;
|
private boolean adaptivePlayback, directSubmit, fusedIdrFrame;
|
||||||
private boolean lowLatency;
|
|
||||||
private boolean constrainedHighProfile;
|
private boolean constrainedHighProfile;
|
||||||
private boolean refFrameInvalidationAvc, refFrameInvalidationHevc;
|
private boolean refFrameInvalidationAvc, refFrameInvalidationHevc;
|
||||||
private boolean refFrameInvalidationActive;
|
private boolean refFrameInvalidationActive;
|
||||||
@@ -58,7 +62,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private int consecutiveCrashCount;
|
private int consecutiveCrashCount;
|
||||||
private String glRenderer;
|
private String glRenderer;
|
||||||
private boolean foreground = true;
|
private boolean foreground = true;
|
||||||
private boolean legacyFrameDropRendering = false;
|
|
||||||
private PerfOverlayListener perfListener;
|
private PerfOverlayListener perfListener;
|
||||||
|
|
||||||
private MediaFormat inputFormat;
|
private MediaFormat inputFormat;
|
||||||
@@ -81,9 +84,15 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private int refreshRate;
|
private int refreshRate;
|
||||||
private PreferenceConfiguration prefs;
|
private PreferenceConfiguration prefs;
|
||||||
|
|
||||||
|
private LinkedBlockingQueue<Integer> outputBufferQueue = new LinkedBlockingQueue<>();
|
||||||
|
private static final int OUTPUT_BUFFER_QUEUE_LIMIT = 2;
|
||||||
|
private long lastRenderedFrameTimeNanos;
|
||||||
|
|
||||||
private int numSpsIn;
|
private int numSpsIn;
|
||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
private int numVpsIn;
|
private int numVpsIn;
|
||||||
|
private int numFramesIn;
|
||||||
|
private int numFramesOut;
|
||||||
|
|
||||||
private MediaCodecInfo findAvcDecoder() {
|
private MediaCodecInfo findAvcDecoder() {
|
||||||
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
|
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
|
||||||
@@ -94,7 +103,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MediaCodecInfo findHevcDecoder(PreferenceConfiguration prefs, boolean meteredNetwork, boolean requestedHdr) {
|
private MediaCodecInfo findHevcDecoder(PreferenceConfiguration prefs, boolean meteredNetwork, boolean requestedHdr) {
|
||||||
// Don't return anything if H.265 is forced off
|
// Don't return anything if HEVC is forced off
|
||||||
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
|
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -106,11 +115,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// for even required levels of HEVC.
|
// for even required levels of HEVC.
|
||||||
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
||||||
if (decoderInfo != null) {
|
if (decoderInfo != null) {
|
||||||
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork)) {
|
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork, prefs)) {
|
||||||
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
|
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
|
||||||
|
|
||||||
// HDR implies HEVC forced on, since HEVCMain10HDR10 is required for HDR
|
// HDR implies HEVC forced on, since HEVCMain10HDR10 is required for HDR.
|
||||||
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_ON || requestedHdr) {
|
// > 4K streaming also requires HEVC, so force it on there too.
|
||||||
|
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_ON || requestedHdr ||
|
||||||
|
prefs.width > 4096 || prefs.height > 4096) {
|
||||||
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
|
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -194,15 +205,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return avcDecoder != null;
|
return avcDecoder != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBlacklistedForFrameRate(int frameRate) {
|
|
||||||
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedForFrameRate(avcDecoder.getName(), frameRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enableLegacyFrameDropRendering() {
|
|
||||||
LimeLog.info("Legacy frame drop rendering enabled");
|
|
||||||
legacyFrameDropRendering = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isHevcMain10Hdr10Supported() {
|
public boolean isHevcMain10Hdr10Supported() {
|
||||||
if (hevcDecoder == null) {
|
if (hevcDecoder == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -238,43 +240,45 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
this.refreshRate = redrawRate;
|
this.refreshRate = redrawRate;
|
||||||
|
|
||||||
String mimeType;
|
String mimeType;
|
||||||
String selectedDecoderName;
|
MediaCodecInfo selectedDecoderInfo;
|
||||||
|
|
||||||
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
|
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
|
||||||
mimeType = "video/avc";
|
mimeType = "video/avc";
|
||||||
selectedDecoderName = avcDecoder.getName();
|
selectedDecoderInfo = avcDecoder;
|
||||||
|
|
||||||
if (avcDecoder == null) {
|
if (avcDecoder == null) {
|
||||||
LimeLog.severe("No available AVC decoder!");
|
LimeLog.severe("No available AVC decoder!");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (width > 4096 || height > 4096) {
|
||||||
|
LimeLog.severe("> 4K streaming only supported on HEVC");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// These fixups only apply to H264 decoders
|
// These fixups only apply to H264 decoders
|
||||||
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(selectedDecoderName);
|
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(selectedDecoderInfo.getName());
|
||||||
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(selectedDecoderName);
|
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(selectedDecoderInfo.getName());
|
||||||
constrainedHighProfile = MediaCodecHelper.decoderNeedsConstrainedHighProfile(selectedDecoderName);
|
constrainedHighProfile = MediaCodecHelper.decoderNeedsConstrainedHighProfile(selectedDecoderInfo.getName());
|
||||||
isExynos4 = MediaCodecHelper.isExynos4Device();
|
isExynos4 = MediaCodecHelper.isExynos4Device();
|
||||||
if (needsSpsBitstreamFixup) {
|
if (needsSpsBitstreamFixup) {
|
||||||
LimeLog.info("Decoder "+selectedDecoderName+" needs SPS bitstream restrictions fixup");
|
LimeLog.info("Decoder "+selectedDecoderInfo.getName()+" needs SPS bitstream restrictions fixup");
|
||||||
}
|
}
|
||||||
if (needsBaselineSpsHack) {
|
if (needsBaselineSpsHack) {
|
||||||
LimeLog.info("Decoder "+selectedDecoderName+" needs baseline SPS hack");
|
LimeLog.info("Decoder "+selectedDecoderInfo.getName()+" needs baseline SPS hack");
|
||||||
}
|
}
|
||||||
if (constrainedHighProfile) {
|
if (constrainedHighProfile) {
|
||||||
LimeLog.info("Decoder "+selectedDecoderName+" needs constrained high profile");
|
LimeLog.info("Decoder "+selectedDecoderInfo.getName()+" needs constrained high profile");
|
||||||
}
|
}
|
||||||
if (isExynos4) {
|
if (isExynos4) {
|
||||||
LimeLog.info("Decoder "+selectedDecoderName+" is on Exynos 4");
|
LimeLog.info("Decoder "+selectedDecoderInfo.getName()+" is on Exynos 4");
|
||||||
}
|
}
|
||||||
|
|
||||||
refFrameInvalidationActive = refFrameInvalidationAvc;
|
refFrameInvalidationActive = refFrameInvalidationAvc;
|
||||||
|
|
||||||
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(avcDecoder, mimeType);
|
|
||||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder, mimeType);
|
|
||||||
}
|
}
|
||||||
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
|
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
|
||||||
mimeType = "video/hevc";
|
mimeType = "video/hevc";
|
||||||
selectedDecoderName = hevcDecoder.getName();
|
selectedDecoderInfo = hevcDecoder;
|
||||||
|
|
||||||
if (hevcDecoder == null) {
|
if (hevcDecoder == null) {
|
||||||
LimeLog.severe("No available HEVC decoder!");
|
LimeLog.severe("No available HEVC decoder!");
|
||||||
@@ -282,9 +286,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refFrameInvalidationActive = refFrameInvalidationHevc;
|
refFrameInvalidationActive = refFrameInvalidationHevc;
|
||||||
|
|
||||||
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(hevcDecoder, mimeType);
|
|
||||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(hevcDecoder, mimeType);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Unknown format
|
// Unknown format
|
||||||
@@ -292,10 +293,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return -3;
|
return -3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(selectedDecoderInfo, mimeType);
|
||||||
|
fusedIdrFrame = MediaCodecHelper.decoderSupportsFusedIdrFrame(selectedDecoderInfo, mimeType);
|
||||||
|
|
||||||
// Codecs have been known to throw all sorts of crazy runtime exceptions
|
// Codecs have been known to throw all sorts of crazy runtime exceptions
|
||||||
// due to implementation problems
|
// due to implementation problems
|
||||||
try {
|
try {
|
||||||
videoDecoder = MediaCodec.createByCodecName(selectedDecoderName);
|
videoDecoder = MediaCodec.createByCodecName(selectedDecoderInfo.getName());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return -4;
|
return -4;
|
||||||
@@ -305,10 +309,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
// Avoid setting KEY_FRAME_RATE on Lollipop and earlier to reduce compatibility risk
|
// Avoid setting KEY_FRAME_RATE on Lollipop and earlier to reduce compatibility risk
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// We use prefs.fps instead of redrawRate here because the low latency hack in Game.java
|
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, redrawRate);
|
||||||
// may leave us with an odd redrawRate value like 59 or 49 which might cause the decoder
|
|
||||||
// to puke. To be safe, we'll use the unmodified value.
|
|
||||||
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, prefs.fps);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
|
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
|
||||||
@@ -318,28 +319,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lowLatency) {
|
MediaCodecHelper.setDecoderLowLatencyOptions(videoFormat, selectedDecoderInfo, mimeType);
|
||||||
videoFormat.setInteger(MediaCodecHelper.KEY_LOW_LATENCY, 1);
|
|
||||||
}
|
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
// Set the Qualcomm vendor low latency extension if the Android R option is unavailable
|
|
||||||
if (MediaCodecHelper.decoderSupportsQcomVendorLowLatency(selectedDecoderName)) {
|
|
||||||
// MediaCodec supports vendor-defined format keys using the "vendor.<extension name>.<parameter name>" syntax.
|
|
||||||
// These allow access to functionality that is not exposed through documented MediaFormat.KEY_* values.
|
|
||||||
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/common/inc/vidc_vendor_extensions.h;l=67
|
|
||||||
//
|
|
||||||
// Examples of Qualcomm's vendor extensions for Snapdragon 845:
|
|
||||||
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/vdec/src/omx_vdec_extensions.hpp
|
|
||||||
// https://cs.android.com/android/_/android/platform/hardware/qcom/sm8150/media/+/0621ceb1c1b19564999db8293574a0e12952ff6c
|
|
||||||
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Operate at maximum rate to lower latency as much as possible on
|
|
||||||
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
|
|
||||||
// but that will actually result in the decoder crashing if it can't satisfy
|
|
||||||
// our (ludicrous) operating rate requirement.
|
|
||||||
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
configuredFormat = videoFormat;
|
configuredFormat = videoFormat;
|
||||||
LimeLog.info("Configuring with format: "+configuredFormat);
|
LimeLog.info("Configuring with format: "+configuredFormat);
|
||||||
@@ -369,7 +349,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Using codec "+selectedDecoderName+" for hardware decoding "+mimeType);
|
LimeLog.info("Using codec "+selectedDecoderInfo.getName()+" for hardware decoding "+mimeType);
|
||||||
|
|
||||||
// Start the decoder
|
// Start the decoder
|
||||||
videoDecoder.start();
|
videoDecoder.start();
|
||||||
@@ -412,7 +392,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
//
|
//
|
||||||
if (initialException != null) {
|
if (initialException != null) {
|
||||||
// This isn't the first time we've had an exception processing video
|
// This isn't the first time we've had an exception processing video
|
||||||
if (System.currentTimeMillis() - initialExceptionTimestamp >= EXCEPTION_REPORT_DELAY_MS) {
|
if (SystemClock.uptimeMillis() - initialExceptionTimestamp >= EXCEPTION_REPORT_DELAY_MS) {
|
||||||
// It's been over 3 seconds and we're still getting exceptions. Throw the original now.
|
// It's been over 3 seconds and we're still getting exceptions. Throw the original now.
|
||||||
if (!reportedCrash) {
|
if (!reportedCrash) {
|
||||||
reportedCrash = true;
|
reportedCrash = true;
|
||||||
@@ -429,11 +409,51 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
else {
|
else {
|
||||||
initialException = new RendererException(this, e);
|
initialException = new RendererException(this, e);
|
||||||
}
|
}
|
||||||
initialExceptionTimestamp = System.currentTimeMillis();
|
initialExceptionTimestamp = SystemClock.uptimeMillis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFrame(long frameTimeNanos) {
|
||||||
|
// Do nothing if we're stopping
|
||||||
|
if (stopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't render unless a new frame is due. This prevents microstutter when streaming
|
||||||
|
// at a frame rate that doesn't match the display (such as 60 FPS on 120 Hz).
|
||||||
|
long actualFrameTimeDeltaNs = frameTimeNanos - lastRenderedFrameTimeNanos;
|
||||||
|
long expectedFrameTimeDeltaNs = 800000000 / refreshRate; // within 80% of the next frame
|
||||||
|
if (actualFrameTimeDeltaNs >= expectedFrameTimeDeltaNs) {
|
||||||
|
// Render up to one frame when in frame pacing mode.
|
||||||
|
//
|
||||||
|
// NB: Since the queue limit is 2, we won't starve the decoder of output buffers
|
||||||
|
// by holding onto them for too long. This also ensures we will have that 1 extra
|
||||||
|
// frame of buffer to smooth over network/rendering jitter.
|
||||||
|
Integer nextOutputBuffer = outputBufferQueue.poll();
|
||||||
|
if (nextOutputBuffer != null) {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
videoDecoder.releaseOutputBuffer(nextOutputBuffer, frameTimeNanos);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
videoDecoder.releaseOutputBuffer(nextOutputBuffer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRenderedFrameTimeNanos = frameTimeNanos;
|
||||||
|
activeWindowVideoStats.totalFramesRendered++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// This will leak nextOutputBuffer, but there's really nothing else we can do
|
||||||
|
handleDecoderException(e, null, 0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request another callback for next frame
|
||||||
|
Choreographer.getInstance().postFrameCallback(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void startRendererThread()
|
private void startRendererThread()
|
||||||
{
|
{
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@@ -448,32 +468,60 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
long presentationTimeUs = info.presentationTimeUs;
|
long presentationTimeUs = info.presentationTimeUs;
|
||||||
int lastIndex = outIndex;
|
int lastIndex = outIndex;
|
||||||
|
|
||||||
// Get the last output buffer in the queue
|
numFramesOut++;
|
||||||
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
|
||||||
|
|
||||||
lastIndex = outIndex;
|
// Render the latest frame now if frame pacing isn't in balanced mode
|
||||||
presentationTimeUs = info.presentationTimeUs;
|
if (prefs.framePacing != PreferenceConfiguration.FRAME_PACING_BALANCED) {
|
||||||
}
|
// Get the last output buffer in the queue
|
||||||
|
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
||||||
|
|
||||||
// Render the last buffer
|
numFramesOut++;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
if (legacyFrameDropRendering) {
|
lastIndex = outIndex;
|
||||||
// Use a PTS that will cause this frame to be dropped if another comes in within
|
presentationTimeUs = info.presentationTimeUs;
|
||||||
// the same V-sync period
|
}
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime());
|
|
||||||
|
if (prefs.framePacing == PreferenceConfiguration.FRAME_PACING_MAX_SMOOTHNESS ||
|
||||||
|
prefs.framePacing == PreferenceConfiguration.FRAME_PACING_CAP_FPS) {
|
||||||
|
// In max smoothness or cap FPS mode, we want to never drop frames
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// Use a PTS that will cause this frame to never be dropped
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// is disabled
|
// Use a PTS that will cause this frame to be dropped if another comes in within
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
// the same V-sync period
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeWindowVideoStats.totalFramesRendered++;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
// For balanced frame pacing case, the Choreographer callback will handle rendering.
|
||||||
}
|
// We just put all frames into the output buffer queue and let it handle things.
|
||||||
|
|
||||||
activeWindowVideoStats.totalFramesRendered++;
|
// Discard the oldest buffer if we've exceeded our limit.
|
||||||
|
//
|
||||||
|
// NB: We have to do this on the producer side because the consumer may not
|
||||||
|
// run for a while (if there is a huge mismatch between stream FPS and display
|
||||||
|
// refresh rate).
|
||||||
|
if (outputBufferQueue.size() == OUTPUT_BUFFER_QUEUE_LIMIT) {
|
||||||
|
videoDecoder.releaseOutputBuffer(outputBufferQueue.take(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this buffer
|
||||||
|
outputBufferQueue.add(lastIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||||
@@ -548,6 +596,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
startRendererThread();
|
startRendererThread();
|
||||||
|
|
||||||
|
// Start Choreographer callbacks for rendering with frame pacing in balanced mode
|
||||||
|
// NB: This must be done on a thread with a looper!
|
||||||
|
if (prefs.framePacing == PreferenceConfiguration.FRAME_PACING_BALANCED) {
|
||||||
|
Handler h = new Handler(Looper.getMainLooper());
|
||||||
|
h.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Choreographer.getInstance().postFrameCallback(MediaCodecDecoderRenderer.this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// !!! May be called even if setup()/start() fails !!!
|
// !!! May be called even if setup()/start() fails !!!
|
||||||
@@ -559,6 +619,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
if (rendererThread != null) {
|
if (rendererThread != null) {
|
||||||
rendererThread.interrupt();
|
rendererThread.interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Halt further Choreographer callbacks
|
||||||
|
if (prefs.framePacing == PreferenceConfiguration.FRAME_PACING_BALANCED) {
|
||||||
|
Handler h = new Handler(Looper.getMainLooper());
|
||||||
|
h.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Choreographer.getInstance().removeFrameCallback(MediaCodecDecoderRenderer.this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -569,7 +640,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// Wait for the renderer thread to shut down
|
// Wait for the renderer thread to shut down
|
||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException ignored) { }
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -577,6 +655,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
videoDecoder.release();
|
videoDecoder.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHdrMode(boolean enabled) {
|
||||||
|
// TODO: Set HDR metadata?
|
||||||
|
}
|
||||||
|
|
||||||
private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
|
private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
|
||||||
try {
|
try {
|
||||||
videoDecoder.queueInputBuffer(inputBufferIndex,
|
videoDecoder.queueInputBuffer(inputBufferIndex,
|
||||||
@@ -632,14 +715,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||||
int frameNumber, long receiveTimeMs) {
|
int frameNumber, int frameType, long receiveTimeMs, long enqueueTimeMs) {
|
||||||
if (stopping) {
|
if (stopping) {
|
||||||
// Don't bother if we're stopping
|
// Don't bother if we're stopping
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastFrameNumber == 0) {
|
if (lastFrameNumber == 0) {
|
||||||
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
activeWindowVideoStats.measurementStartTimestamp = SystemClock.uptimeMillis();
|
||||||
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
||||||
// We can receive the same "frame" multiple times if it's an IDR frame.
|
// We can receive the same "frame" multiple times if it's an IDR frame.
|
||||||
// In that case, each frame start NALU is submitted independently.
|
// In that case, each frame start NALU is submitted independently.
|
||||||
@@ -651,7 +734,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
lastFrameNumber = frameNumber;
|
lastFrameNumber = frameNumber;
|
||||||
|
|
||||||
// Flip stats windows roughly every second
|
// Flip stats windows roughly every second
|
||||||
if (System.currentTimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) {
|
if (SystemClock.uptimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) {
|
||||||
if (prefs.enablePerfOverlay) {
|
if (prefs.enablePerfOverlay) {
|
||||||
VideoStats lastTwo = new VideoStats();
|
VideoStats lastTwo = new VideoStats();
|
||||||
lastTwo.add(lastWindowVideoStats);
|
lastTwo.add(lastWindowVideoStats);
|
||||||
@@ -668,46 +751,29 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float decodeTimeMs = (float)lastTwo.decoderTimeMs / lastTwo.totalFramesReceived;
|
float decodeTimeMs = (float)lastTwo.decoderTimeMs / lastTwo.totalFramesReceived;
|
||||||
String perfText = context.getString(
|
long rttInfo = MoonBridge.getEstimatedRttInfo();
|
||||||
R.string.perf_overlay_text,
|
StringBuilder sb = new StringBuilder();
|
||||||
initialWidth + "x" + initialHeight,
|
sb.append(context.getString(R.string.perf_overlay_streamdetails, initialWidth + "x" + initialHeight, fps.totalFps)).append('\n');
|
||||||
decoder,
|
sb.append(context.getString(R.string.perf_overlay_decoder, decoder)).append('\n');
|
||||||
fps.totalFps,
|
sb.append(context.getString(R.string.perf_overlay_incomingfps, fps.receivedFps)).append('\n');
|
||||||
fps.receivedFps,
|
sb.append(context.getString(R.string.perf_overlay_renderingfps, fps.renderedFps)).append('\n');
|
||||||
fps.renderedFps,
|
sb.append(context.getString(R.string.perf_overlay_netdrops,
|
||||||
(float)lastTwo.framesLost / lastTwo.totalFrames * 100,
|
(float)lastTwo.framesLost / lastTwo.totalFrames * 100)).append('\n');
|
||||||
((float)lastTwo.totalTimeMs / lastTwo.totalFramesReceived) - decodeTimeMs,
|
sb.append(context.getString(R.string.perf_overlay_netlatency,
|
||||||
decodeTimeMs);
|
(int)(rttInfo >> 32), (int)rttInfo)).append('\n');
|
||||||
perfListener.onPerfUpdate(perfText);
|
sb.append(context.getString(R.string.perf_overlay_dectime, decodeTimeMs));
|
||||||
|
perfListener.onPerfUpdate(sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
globalVideoStats.add(activeWindowVideoStats);
|
globalVideoStats.add(activeWindowVideoStats);
|
||||||
lastWindowVideoStats.copy(activeWindowVideoStats);
|
lastWindowVideoStats.copy(activeWindowVideoStats);
|
||||||
activeWindowVideoStats.clear();
|
activeWindowVideoStats.clear();
|
||||||
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
activeWindowVideoStats.measurementStartTimestamp = SystemClock.uptimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
activeWindowVideoStats.totalFramesReceived++;
|
|
||||||
activeWindowVideoStats.totalFrames++;
|
|
||||||
|
|
||||||
int inputBufferIndex;
|
int inputBufferIndex;
|
||||||
ByteBuffer buf;
|
ByteBuffer buf;
|
||||||
|
long timestampUs;
|
||||||
long timestampUs = System.nanoTime() / 1000;
|
|
||||||
|
|
||||||
if (!FRAME_RENDER_TIME_ONLY) {
|
|
||||||
// Count time from first packet received to decode start
|
|
||||||
activeWindowVideoStats.totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestampUs <= lastTimestampUs) {
|
|
||||||
// We can't submit multiple buffers with the same timestamp
|
|
||||||
// so bump it up by one before queuing
|
|
||||||
timestampUs = lastTimestampUs + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTimestampUs = timestampUs;
|
|
||||||
|
|
||||||
int codecFlags = 0;
|
int codecFlags = 0;
|
||||||
|
|
||||||
// H264 SPS
|
// H264 SPS
|
||||||
@@ -847,9 +913,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
numPpsIn++;
|
numPpsIn++;
|
||||||
|
|
||||||
// If this is the first CSD blob or we aren't supporting
|
// If this is the first CSD blob or we aren't supporting
|
||||||
// adaptive playback, we will submit the CSD blob in a
|
// fused IDR frames, we will submit the CSD blob in a
|
||||||
// separate input buffer.
|
// separate input buffer.
|
||||||
if (!submittedCsd || !adaptivePlayback) {
|
if (!submittedCsd || !fusedIdrFrame) {
|
||||||
inputBufferIndex = dequeueInputBuffer();
|
inputBufferIndex = dequeueInputBuffer();
|
||||||
if (inputBufferIndex < 0) {
|
if (inputBufferIndex < 0) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
@@ -873,6 +939,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
// This is the CSD blob
|
// This is the CSD blob
|
||||||
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
||||||
|
timestampUs = 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Batch this to submit together with the next I-frame
|
// Batch this to submit together with the next I-frame
|
||||||
@@ -886,6 +953,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
activeWindowVideoStats.totalFramesReceived++;
|
||||||
|
activeWindowVideoStats.totalFrames++;
|
||||||
|
|
||||||
|
if (!FRAME_RENDER_TIME_ONLY) {
|
||||||
|
// Count time from first packet received to enqueue time as receive time
|
||||||
|
// We will count DU queue time as part of decoding, because it is directly
|
||||||
|
// caused by a slow decoder.
|
||||||
|
activeWindowVideoStats.totalTimeMs += enqueueTimeMs - receiveTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
inputBufferIndex = dequeueInputBuffer();
|
inputBufferIndex = dequeueInputBuffer();
|
||||||
if (inputBufferIndex < 0) {
|
if (inputBufferIndex < 0) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
@@ -911,6 +988,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
submitCsdNextCall = false;
|
submitCsdNextCall = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frameType == MoonBridge.FRAME_TYPE_IDR) {
|
||||||
|
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
timestampUs = enqueueTimeMs * 1000;
|
||||||
|
|
||||||
|
if (timestampUs <= lastTimestampUs) {
|
||||||
|
// We can't submit multiple buffers with the same timestamp
|
||||||
|
// so bump it up by one before queuing
|
||||||
|
timestampUs = lastTimestampUs + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimestampUs = timestampUs;
|
||||||
|
|
||||||
|
numFramesIn++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decodeUnitLength > buf.limit() - buf.position()) {
|
if (decodeUnitLength > buf.limit() - buf.position()) {
|
||||||
@@ -980,8 +1073,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// Queue the new SPS
|
// Queue the new SPS
|
||||||
return queueInputBuffer(inputIndex,
|
return queueInputBuffer(inputIndex,
|
||||||
0, inputBuffer.position(),
|
0, inputBuffer.position(),
|
||||||
System.nanoTime() / 1000,
|
0, MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
||||||
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1056,8 +1148,31 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) {
|
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) {
|
||||||
String str = "";
|
String str;
|
||||||
|
|
||||||
|
if (renderer.numVpsIn == 0 && renderer.numSpsIn == 0 && renderer.numPpsIn == 0) {
|
||||||
|
str = "PreSPSError";
|
||||||
|
}
|
||||||
|
else if (renderer.numSpsIn > 0 && renderer.numPpsIn == 0) {
|
||||||
|
str = "PrePPSError";
|
||||||
|
}
|
||||||
|
else if (renderer.numPpsIn > 0 && renderer.numFramesIn == 0) {
|
||||||
|
str = "PreIFrameError";
|
||||||
|
}
|
||||||
|
else if (renderer.numFramesIn > 0 && renderer.outputFormat == null) {
|
||||||
|
str = "PreOutputConfigError";
|
||||||
|
}
|
||||||
|
else if (renderer.outputFormat != null && renderer.numFramesOut == 0) {
|
||||||
|
str = "PreOutputError";
|
||||||
|
}
|
||||||
|
else if (renderer.numFramesOut <= renderer.refreshRate * 30) {
|
||||||
|
str = "EarlyOutputError";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str = "ErrorWhileStreaming";
|
||||||
|
}
|
||||||
|
|
||||||
|
str += ": 1\n";
|
||||||
str += "Format: "+String.format("%x", renderer.videoFormat)+"\n";
|
str += "Format: "+String.format("%x", renderer.videoFormat)+"\n";
|
||||||
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
||||||
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
||||||
@@ -1091,20 +1206,26 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
|
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
|
||||||
str += "GL Renderer: "+renderer.glRenderer+"\n";
|
str += "GL Renderer: "+renderer.glRenderer+"\n";
|
||||||
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
str += "SOC: "+Build.SOC_MANUFACTURER+" - "+Build.SOC_MODEL+"\n";
|
||||||
|
str += "Performance class: "+Build.VERSION.MEDIA_PERFORMANCE_CLASS+"\n";
|
||||||
|
}
|
||||||
str += "Foreground: "+renderer.foreground+"\n";
|
str += "Foreground: "+renderer.foreground+"\n";
|
||||||
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
||||||
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
|
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
|
||||||
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n";
|
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n";
|
||||||
str += "Low latency mode: "+renderer.lowLatency+"\n";
|
str += "Fused IDR frames: "+renderer.fusedIdrFrame+"\n";
|
||||||
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||||
str += "FPS target: "+renderer.refreshRate+"\n";
|
str += "FPS target: "+renderer.refreshRate+"\n";
|
||||||
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
||||||
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
str += "CSD stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
||||||
|
str += "Frames in-out: "+renderer.numFramesIn+", "+renderer.numFramesOut+"\n";
|
||||||
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
|
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
|
||||||
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
|
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
|
||||||
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n";
|
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n";
|
||||||
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
|
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
|
||||||
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
|
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
|
||||||
|
str += "Frame pacing mode: "+renderer.prefs.framePacing+"\n";
|
||||||
|
|
||||||
if (currentBuffer != null) {
|
if (currentBuffer != null) {
|
||||||
str += "Current buffer: ";
|
str += "Current buffer: ";
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ import android.media.MediaCodecInfo;
|
|||||||
import android.media.MediaCodecList;
|
import android.media.MediaCodecList;
|
||||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||||
|
import android.media.MediaFormat;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
public class MediaCodecHelper {
|
public class MediaCodecHelper {
|
||||||
|
|
||||||
@@ -36,15 +38,14 @@ public class MediaCodecHelper {
|
|||||||
private static final List<String> whitelistedHevcDecoders;
|
private static final List<String> whitelistedHevcDecoders;
|
||||||
private static final List<String> refFrameInvalidationAvcPrefixes;
|
private static final List<String> refFrameInvalidationAvcPrefixes;
|
||||||
private static final List<String> refFrameInvalidationHevcPrefixes;
|
private static final List<String> refFrameInvalidationHevcPrefixes;
|
||||||
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
|
||||||
private static final List<String> blacklisted59FpsDecoderPrefixes;
|
|
||||||
private static final List<String> qualcommDecoderPrefixes;
|
private static final List<String> qualcommDecoderPrefixes;
|
||||||
|
private static final List<String> kirinDecoderPrefixes;
|
||||||
|
private static final List<String> exynosDecoderPrefixes;
|
||||||
|
|
||||||
// FIXME: Remove when Android R SDK is finalized
|
public static final boolean IS_EMULATOR = Build.HARDWARE.equals("ranchu") || Build.HARDWARE.equals("cheets");
|
||||||
public static final String FEATURE_LowLatency = "low-latency";
|
|
||||||
public static final String KEY_LOW_LATENCY = "low-latency";
|
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragon = false;
|
private static boolean isLowEndSnapdragon = false;
|
||||||
|
private static boolean isAdreno620 = false;
|
||||||
private static boolean initialized = false;
|
private static boolean initialized = false;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@@ -81,7 +82,7 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
// Blacklist software decoders that don't support H264 high profile,
|
// Blacklist software decoders that don't support H264 high profile,
|
||||||
// but exclude the official AOSP and CrOS emulator from this restriction.
|
// but exclude the official AOSP and CrOS emulator from this restriction.
|
||||||
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
|
if (!IS_EMULATOR) {
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ public class MediaCodecHelper {
|
|||||||
// if adaptive playback was enabled so let's avoid it to be safe.
|
// if adaptive playback was enabled so let's avoid it to be safe.
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
||||||
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
|
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
|
||||||
// on some Android TV devices with H.265 only.
|
// on some Android TV devices with HEVC only.
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
|
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
|
||||||
|
|
||||||
constrainedHighProfilePrefixes = new LinkedList<>();
|
constrainedHighProfilePrefixes = new LinkedList<>();
|
||||||
@@ -127,7 +128,7 @@ public class MediaCodecHelper {
|
|||||||
whitelistedHevcDecoders = new LinkedList<>();
|
whitelistedHevcDecoders = new LinkedList<>();
|
||||||
|
|
||||||
// Allow software HEVC decoding in the official AOSP emulator
|
// Allow software HEVC decoding in the official AOSP emulator
|
||||||
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
|
if (Build.HARDWARE.equals("ranchu")) {
|
||||||
whitelistedHevcDecoders.add("omx.google");
|
whitelistedHevcDecoders.add("omx.google");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +148,10 @@ public class MediaCodecHelper {
|
|||||||
// I know the Fire TV 2 and 3 works, so I'll whitelist Amazon devices which seem to actually be tested.
|
// I know the Fire TV 2 and 3 works, so I'll whitelist Amazon devices which seem to actually be tested.
|
||||||
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
whitelistedHevcDecoders.add("omx.amlogic");
|
|
||||||
|
// This broke at some point on the Fire TV 3 and now the decoder
|
||||||
|
// never produces any output frames.
|
||||||
|
//whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plot twist: On newer Sony devices (BRAVIA_ATV2, BRAVIA_ATV3_4K, BRAVIA_UR1_4K) the H.264 decoder crashes
|
// Plot twist: On newer Sony devices (BRAVIA_ATV2, BRAVIA_ATV3_4K, BRAVIA_UR1_4K) the H.264 decoder crashes
|
||||||
@@ -156,9 +160,21 @@ public class MediaCodecHelper {
|
|||||||
whitelistedHevcDecoders.add("omx.mtk");
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Amlogic requires 1 reference frame for HEVC to avoid hanging. Since it's been years
|
||||||
|
// since GFE added support for maxNumReferenceFrames, we'll just enable all Amlogic SoCs
|
||||||
|
// running Android 9 or later. HEVC is much lower latency than H.264 on Sabrina (S905X2).
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Realtek SoCs are used inside many Android TV devices and can only do 4K60 with HEVC.
|
||||||
|
// We'll enable those HEVC decoders by default and see if anything breaks.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
whitelistedHevcDecoders.add("omx.realtek");
|
||||||
|
}
|
||||||
|
|
||||||
// These theoretically have good HEVC decoding capabilities (potentially better than
|
// These theoretically have good HEVC decoding capabilities (potentially better than
|
||||||
// their AVC decoders), but haven't been tested enough
|
// their AVC decoders), but haven't been tested enough
|
||||||
//whitelistedHevcDecoders.add("omx.amlogic");
|
|
||||||
//whitelistedHevcDecoders.add("omx.rk");
|
//whitelistedHevcDecoders.add("omx.rk");
|
||||||
|
|
||||||
// Let's see if HEVC decoders are finally stable with C2
|
// Let's see if HEVC decoders are finally stable with C2
|
||||||
@@ -176,24 +192,6 @@ public class MediaCodecHelper {
|
|||||||
// Qualcomm is currently the only decoders in this group.
|
// Qualcomm is currently the only decoders in this group.
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
|
||||||
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
|
|
||||||
blacklisted59FpsDecoderPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// We see a bunch of crashes on MediaTek Android TVs running
|
|
||||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
|
||||||
// these devices and hope they fix it in Pie.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
||||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
|
||||||
|
|
||||||
// 59 FPS also seems to crash on the Sony Bravia TV ATV3 model.
|
|
||||||
// Blacklist that frame rate on these devices too.
|
|
||||||
if (Build.DEVICE.startsWith("BRAVIA_ATV3")) {
|
|
||||||
blacklisted59FpsDecoderPrefixes.add("omx.mtk");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
qualcommDecoderPrefixes = new LinkedList<>();
|
qualcommDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
@@ -201,6 +199,18 @@ public class MediaCodecHelper {
|
|||||||
qualcommDecoderPrefixes.add("c2.qti");
|
qualcommDecoderPrefixes.add("c2.qti");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
kirinDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
|
kirinDecoderPrefixes.add("omx.hisi");
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
exynosDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
|
exynosDecoderPrefixes.add("omx.exynos");
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isPowerVR(String glRenderer) {
|
private static boolean isPowerVR(String glRenderer) {
|
||||||
return glRenderer.toLowerCase().contains("powervr");
|
return glRenderer.toLowerCase().contains("powervr");
|
||||||
}
|
}
|
||||||
@@ -235,19 +245,23 @@ public class MediaCodecHelper {
|
|||||||
return modelNumber.charAt(1) == '0';
|
return modelNumber.charAt(1) == '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int getAdrenoRendererModelNumber(String glRenderer) {
|
||||||
|
String modelNumber = getAdrenoVersionString(glRenderer);
|
||||||
|
if (modelNumber == null) {
|
||||||
|
// Not an Adreno GPU
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Integer.parseInt(modelNumber);
|
||||||
|
}
|
||||||
|
|
||||||
// This is a workaround for some broken devices that report
|
// This is a workaround for some broken devices that report
|
||||||
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
|
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
|
||||||
// An example of such a device is the Huawei Honor 5x with the
|
// An example of such a device is the Huawei Honor 5x with the
|
||||||
// Snapdragon 616 SoC (Adreno 405).
|
// Snapdragon 616 SoC (Adreno 405).
|
||||||
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
|
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
|
||||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
|
||||||
if (modelNumber == null) {
|
|
||||||
// Not an Adreno GPU
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapdragon 4xx and higher support GLES 3.1
|
// Snapdragon 4xx and higher support GLES 3.1
|
||||||
return modelNumber.charAt(0) >= '4';
|
return getAdrenoRendererModelNumber(glRenderer) >= 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initialize(Context context, String glRenderer) {
|
public static void initialize(Context context, String glRenderer) {
|
||||||
@@ -262,6 +276,7 @@ public class MediaCodecHelper {
|
|||||||
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
||||||
|
|
||||||
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
|
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
|
||||||
|
isAdreno620 = getAdrenoRendererModelNumber(glRenderer) == 620;
|
||||||
|
|
||||||
// Tegra K1 and later can do reference frame invalidation properly
|
// Tegra K1 and later can do reference frame invalidation properly
|
||||||
if (configInfo.reqGlEsVersion >= 0x30000) {
|
if (configInfo.reqGlEsVersion >= 0x30000) {
|
||||||
@@ -342,12 +357,82 @@ public class MediaCodecHelper {
|
|||||||
return System.nanoTime() / 1000000L;
|
return System.nanoTime() / 1000000L;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderSupportsLowLatency(MediaCodecInfo decoderInfo, String mimeType) {
|
private static boolean decoderSupportsAndroidRLowLatency(MediaCodecInfo decoderInfo, String mimeType) {
|
||||||
// KitKat added CodecCapabilities.isFeatureSupported()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
try {
|
||||||
|
if (decoderInfo.getCapabilitiesForType(mimeType).isFeatureSupported(CodecCapabilities.FEATURE_LowLatency)) {
|
||||||
|
LimeLog.info("Low latency decoding mode supported (FEATURE_LowLatency)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Tolerate buggy codecs
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean decoderSupportsMaxOperatingRate(String decoderName) {
|
||||||
|
// Operate at maximum rate to lower latency as much as possible on
|
||||||
|
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
|
||||||
|
// but that will actually result in the decoder crashing if it can't satisfy
|
||||||
|
// our (ludicrous) operating rate requirement. This seems to cause reliable
|
||||||
|
// crashes on the Xiaomi Mi 10 lite 5G and Redmi K30i 5G on Android 10, so
|
||||||
|
// we'll disable it on Snapdragon 765G and all non-Qualcomm devices to be safe.
|
||||||
|
//
|
||||||
|
// NB: Even on Android 10, this optimization still provides significant
|
||||||
|
// performance gains on Pixel 2.
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||||
|
isDecoderInList(qualcommDecoderPrefixes, decoderName) &&
|
||||||
|
!isAdreno620;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setDecoderLowLatencyOptions(MediaFormat videoFormat, MediaCodecInfo decoderInfo, String mimeType) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && decoderSupportsAndroidRLowLatency(decoderInfo, mimeType)) {
|
||||||
|
videoFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
// MediaCodec supports vendor-defined format keys using the "vendor.<extension name>.<parameter name>" syntax.
|
||||||
|
// These allow access to functionality that is not exposed through documented MediaFormat.KEY_* values.
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/common/inc/vidc_vendor_extensions.h;l=67
|
||||||
|
//
|
||||||
|
// MediaCodec vendor extension support was introduced in Android 8.0:
|
||||||
|
// https://cs.android.com/android/_/android/platform/frameworks/av/+/01c10f8cdcd58d1e7025f426a72e6e75ba5d7fc2
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// Try vendor-specific low latency options
|
||||||
|
if (isDecoderInList(qualcommDecoderPrefixes, decoderInfo.getName())) {
|
||||||
|
// Examples of Qualcomm's vendor extensions for Snapdragon 845:
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/vdec/src/omx_vdec_extensions.hpp
|
||||||
|
// https://cs.android.com/android/_/android/platform/hardware/qcom/sm8150/media/+/0621ceb1c1b19564999db8293574a0e12952ff6c
|
||||||
|
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
|
||||||
|
}
|
||||||
|
else if (isDecoderInList(kirinDecoderPrefixes, decoderInfo.getName())) {
|
||||||
|
// Kirin low latency options
|
||||||
|
// https://developer.huawei.com/consumer/cn/forum/topic/0202325564295980115
|
||||||
|
videoFormat.setInteger("vendor.hisi-ext-low-latency-video-dec.video-scene-for-low-latency-req", 1);
|
||||||
|
videoFormat.setInteger("vendor.hisi-ext-low-latency-video-dec.video-scene-for-low-latency-rdy", -1);
|
||||||
|
}
|
||||||
|
else if (isDecoderInList(exynosDecoderPrefixes, decoderInfo.getName())) {
|
||||||
|
// Exynos low latency option for H.264 decoder
|
||||||
|
videoFormat.setInteger("vendor.rtc-ext-dec-low-latency.enable", 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MediaCodecHelper.decoderSupportsMaxOperatingRate(decoderInfo.getName())) {
|
||||||
|
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderSupportsFusedIdrFrame(MediaCodecInfo decoderInfo, String mimeType) {
|
||||||
|
// If adaptive playback is supported, we can submit new CSD together with a keyframe
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
try {
|
try {
|
||||||
if (decoderInfo.getCapabilitiesForType(mimeType).isFeatureSupported(FEATURE_LowLatency)) {
|
if (decoderInfo.getCapabilitiesForType(mimeType).
|
||||||
LimeLog.info("Low latency decoding mode supported (FEATURE_LowLatency)");
|
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
||||||
|
{
|
||||||
|
LimeLog.info("Decoder supports fused IDR frames (FEATURE_AdaptivePlayback)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -384,13 +469,6 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderSupportsQcomVendorLowLatency(String decoderName) {
|
|
||||||
// MediaCodec vendor extension support was introduced in Android 8.0:
|
|
||||||
// https://cs.android.com/android/_/android/platform/frameworks/av/+/01c10f8cdcd58d1e7025f426a72e6e75ba5d7fc2
|
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
|
||||||
isDecoderInList(qualcommDecoderPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
|
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
|
||||||
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
|
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
|
||||||
}
|
}
|
||||||
@@ -407,18 +485,6 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderBlacklistedForFrameRate(String decoderName, int fps) {
|
|
||||||
if (fps == 49) {
|
|
||||||
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
else if (fps == 59) {
|
|
||||||
return isDecoderInList(blacklisted59FpsDecoderPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
||||||
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
||||||
if (videoHeight > 720 && isLowEndSnapdragon) {
|
if (videoHeight > 720 && isLowEndSnapdragon) {
|
||||||
@@ -438,7 +504,7 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
|
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData, PreferenceConfiguration prefs) {
|
||||||
// TODO: Shield Tablet K1/LTE?
|
// TODO: Shield Tablet K1/LTE?
|
||||||
//
|
//
|
||||||
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
||||||
@@ -473,9 +539,10 @@ public class MediaCodecHelper {
|
|||||||
// Some devices have HEVC decoders that we prefer not to use
|
// Some devices have HEVC decoders that we prefer not to use
|
||||||
// typically because it can't support reference frame invalidation.
|
// typically because it can't support reference frame invalidation.
|
||||||
// However, we will use it for HDR and for streaming over mobile networks
|
// However, we will use it for HDR and for streaming over mobile networks
|
||||||
// since it works fine otherwise.
|
// since it works fine otherwise. We will also use it for 4K because RFI
|
||||||
|
// is currently disabled due to issues with video corruption.
|
||||||
if (isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
if (isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
||||||
if (meteredData) {
|
if (meteredData || (prefs.width == 3840 && prefs.height == 2160)) {
|
||||||
LimeLog.info("Selected deprioritized decoder");
|
LimeLog.info("Selected deprioritized decoder");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -542,13 +609,6 @@ public class MediaCodecHelper {
|
|||||||
if (codecInfo.isEncoder()) {
|
if (codecInfo.isEncoder()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip compatibility aliases on Q+
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
if (codecInfo.isAlias()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for preferred decoders
|
// Check for preferred decoders
|
||||||
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
|
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
|
||||||
@@ -564,7 +624,7 @@ public class MediaCodecHelper {
|
|||||||
private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) {
|
private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) {
|
||||||
// Use the new isSoftwareOnly() function on Android Q
|
// Use the new isSoftwareOnly() function on Android Q
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
if (codecInfo.isSoftwareOnly()) {
|
if (!IS_EMULATOR && codecInfo.isSoftwareOnly()) {
|
||||||
LimeLog.info("Skipping software-only decoder: "+codecInfo.getName());
|
LimeLog.info("Skipping software-only decoder: "+codecInfo.getName());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -634,43 +694,57 @@ public class MediaCodecHelper {
|
|||||||
// and we want to be sure all callers are handling this possibility
|
// and we want to be sure all callers are handling this possibility
|
||||||
@SuppressWarnings("RedundantThrows")
|
@SuppressWarnings("RedundantThrows")
|
||||||
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
|
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
// Some devices (Exynos devces, at least) have two sets of decoders.
|
||||||
// Skip encoders
|
// The first set of decoders are C2 which do not support FEATURE_LowLatency,
|
||||||
if (codecInfo.isEncoder()) {
|
// but the second set of OMX decoders do support FEATURE_LowLatency. We want
|
||||||
continue;
|
// to pick the OMX decoders despite the fact that C2 is listed first.
|
||||||
}
|
// On some Qualcomm devices (like Pixel 4), there are separate low latency decoders
|
||||||
|
// (like c2.qti.hevc.decoder.low_latency) that advertise FEATURE_LowLatency while
|
||||||
// Skip compatibility aliases on Q+
|
// the standard ones (like c2.qti.hevc.decoder) do not. Like Exynos, the decoders
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
// with FEATURE_LowLatency support are listed after the standard ones.
|
||||||
if (codecInfo.isAlias()) {
|
for (int i = 0; i < 2; i++) {
|
||||||
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
|
// Skip encoders
|
||||||
|
if (codecInfo.isEncoder()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Find a decoder that supports the requested video format
|
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
|
||||||
if (mime.equalsIgnoreCase(mimeType)) {
|
|
||||||
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
|
||||||
|
|
||||||
// Skip blacklisted codecs
|
// Skip compatibility aliases on Q+
|
||||||
if (isCodecBlacklisted(codecInfo)) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
if (codecInfo.isAlias()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
// Find a decoder that supports the requested video format
|
||||||
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
|
if (mime.equalsIgnoreCase(mimeType)) {
|
||||||
|
LimeLog.info("Examining decoder capabilities of " + codecInfo.getName() + " (round " + (i + 1) + ")");
|
||||||
|
|
||||||
if (requiredProfile != -1) {
|
// Skip blacklisted codecs
|
||||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
if (isCodecBlacklisted(codecInfo)) {
|
||||||
if (profile.profile == requiredProfile) {
|
continue;
|
||||||
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
|
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
||||||
}
|
|
||||||
else {
|
if (i == 0 && !decoderSupportsAndroidRLowLatency(codecInfo, mime)) {
|
||||||
return codecInfo;
|
LimeLog.info("Skipping decoder that lacks FEATURE_LowLatency for round 1");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredProfile != -1) {
|
||||||
|
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||||
|
if (profile.profile == requiredProfile) {
|
||||||
|
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
|
||||||
|
return codecInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
|
||||||
|
} else {
|
||||||
|
return codecInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.limelight.binding.video;
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
class VideoStats {
|
class VideoStats {
|
||||||
|
|
||||||
long decoderTimeMs;
|
long decoderTimeMs;
|
||||||
@@ -24,7 +26,7 @@ class VideoStats {
|
|||||||
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert other.measurementStartTimestamp <= this.measurementStartTimestamp;
|
assert other.measurementStartTimestamp >= this.measurementStartTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void copy(VideoStats other) {
|
void copy(VideoStats other) {
|
||||||
@@ -50,7 +52,7 @@ class VideoStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VideoStatsFps getFps() {
|
VideoStatsFps getFps() {
|
||||||
float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000;
|
float elapsed = (SystemClock.uptimeMillis() - this.measurementStartTimestamp) / (float) 1000;
|
||||||
|
|
||||||
VideoStatsFps fps = new VideoStatsFps();
|
VideoStatsFps fps = new VideoStatsFps();
|
||||||
if (elapsed > 0) {
|
if (elapsed > 0) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.Inet4Address;
|
import java.net.Inet4Address;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.Socket;
|
import java.net.UnknownHostException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -38,6 +38,7 @@ import android.net.NetworkCapabilities;
|
|||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
@@ -46,8 +47,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||||
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
private static final int FAST_POLL_TIMEOUT = 1000;
|
private static final int OFFLINE_POLL_TRIES = 3;
|
||||||
private static final int OFFLINE_POLL_TRIES = 5;
|
|
||||||
private static final int INITIAL_POLL_TRIES = 2;
|
private static final int INITIAL_POLL_TRIES = 2;
|
||||||
private static final int EMPTY_LIST_THRESHOLD = 3;
|
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||||
private static final int POLL_DATA_TTL_MS = 30000;
|
private static final int POLL_DATA_TTL_MS = 30000;
|
||||||
@@ -134,6 +134,18 @@ public class ComputerManagerService extends Service {
|
|||||||
dbManager.updateComputer(existingComputer);
|
dbManager.updateComputer(existingComputer);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
try {
|
||||||
|
// If the active address is a site-local address (RFC 1918),
|
||||||
|
// then use STUN to populate the external address field if
|
||||||
|
// it's not set already.
|
||||||
|
if (details.remoteAddress == null) {
|
||||||
|
InetAddress addr = InetAddress.getByName(details.activeAddress);
|
||||||
|
if (addr.isSiteLocalAddress()) {
|
||||||
|
populateExternalAddress(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException ignored) {}
|
||||||
|
|
||||||
dbManager.updateComputer(details);
|
dbManager.updateComputer(details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +174,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();
|
tuple.lastSuccessfulPollMs = SystemClock.elapsedRealtime();
|
||||||
offlineCount = 0;
|
offlineCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +205,7 @@ public class ComputerManagerService extends Service {
|
|||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
// Enforce the poll data TTL
|
// Enforce the poll data TTL
|
||||||
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
if (SystemClock.elapsedRealtime() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
||||||
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
||||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
}
|
}
|
||||||
@@ -217,7 +229,13 @@ public class ComputerManagerService extends Service {
|
|||||||
// Wait for the bind notification
|
// Wait for the bind notification
|
||||||
discoveryServiceConnection.wait(1000);
|
discoveryServiceConnection.wait(1000);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException ignored) {
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,11 +244,18 @@ public class ComputerManagerService extends Service {
|
|||||||
while (activePolls.get() != 0) {
|
while (activePolls.get() != 0) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(250);
|
Thread.sleep(250);
|
||||||
} catch (InterruptedException ignored) {}
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
public boolean addComputerBlocking(ComputerDetails fakeDetails) throws InterruptedException {
|
||||||
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
|
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,9 +409,18 @@ public class ComputerManagerService extends Service {
|
|||||||
details.ipv6Address = computer.getIpv6Address().getHostAddress();
|
details.ipv6Address = computer.getIpv6Address().getHostAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kick off a serverinfo poll on this machine
|
try {
|
||||||
if (!addComputerBlocking(details)) {
|
// Kick off a blocking serverinfo poll on this machine
|
||||||
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
|
if (!addComputerBlocking(details)) {
|
||||||
|
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,28 +468,25 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
public boolean addComputerBlocking(ComputerDetails fakeDetails) throws InterruptedException {
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
try {
|
|
||||||
// We cannot use runPoll() here because it will attempt to persist the state of the machine
|
// We cannot use runPoll() here because it will attempt to persist the state of the machine
|
||||||
// in the database, which would be bad because we don't have our pinned cert loaded yet.
|
// in the database, which would be bad because we don't have our pinned cert loaded yet.
|
||||||
if (pollComputer(fakeDetails)) {
|
if (pollComputer(fakeDetails)) {
|
||||||
// See if we have record of this PC to pull its pinned cert
|
// See if we have record of this PC to pull its pinned cert
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
if (tuple.computer.uuid.equals(fakeDetails.uuid)) {
|
if (tuple.computer.uuid.equals(fakeDetails.uuid)) {
|
||||||
fakeDetails.serverCert = tuple.computer.serverCert;
|
fakeDetails.serverCert = tuple.computer.serverCert;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll again, possibly with the pinned cert, to get accurate pairing information.
|
|
||||||
// This will insert the host into the database too.
|
|
||||||
runPoll(fakeDetails, true, 0);
|
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return false;
|
// Poll again, possibly with the pinned cert, to get accurate pairing information.
|
||||||
|
// This will insert the host into the database too.
|
||||||
|
runPoll(fakeDetails, true, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the machine is reachable, it was successful
|
// If the machine is reachable, it was successful
|
||||||
@@ -513,11 +544,6 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
|
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
|
||||||
// Fast poll this address first to determine if we can connect at the TCP layer
|
|
||||||
if (!fastPollIp(address)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
||||||
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
@@ -536,146 +562,140 @@ public class ComputerManagerService extends Service {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the new active address
|
|
||||||
newDetails.activeAddress = address;
|
|
||||||
|
|
||||||
return newDetails;
|
return newDetails;
|
||||||
} catch (XmlPullParserException | IOException e) {
|
} catch (XmlPullParserException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just try to establish a TCP connection to speculatively detect a running
|
private static class ParallelPollTuple {
|
||||||
// GFE server
|
public String address;
|
||||||
private boolean fastPollIp(String address) {
|
public ComputerDetails existingDetails;
|
||||||
if (address == null) {
|
|
||||||
// Don't bother if our address is null
|
public boolean complete;
|
||||||
return false;
|
public Thread pollingThread;
|
||||||
|
public ComputerDetails returnedDetails;
|
||||||
|
|
||||||
|
public ParallelPollTuple(String address, ComputerDetails existingDetails) {
|
||||||
|
this.address = address;
|
||||||
|
this.existingDetails = existingDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
Socket s = new Socket();
|
public void interrupt() {
|
||||||
try {
|
if (pollingThread != null) {
|
||||||
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
pollingThread.interrupt();
|
||||||
s.close();
|
}
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startFastPollThread(final String address, final boolean[] info) {
|
private void startParallelPollThread(ParallelPollTuple tuple, HashSet<String> uniqueAddresses) {
|
||||||
Thread t = new Thread() {
|
// Don't bother starting a polling thread for an address that doesn't exist
|
||||||
|
// or if the address has already been polled with an earlier tuple
|
||||||
|
if (tuple.address == null || !uniqueAddresses.add(tuple.address)) {
|
||||||
|
tuple.complete = true;
|
||||||
|
tuple.returnedDetails = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tuple.pollingThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean pollRes = fastPollIp(address);
|
ComputerDetails details = tryPollIp(tuple.existingDetails, tuple.address);
|
||||||
|
|
||||||
synchronized (info) {
|
synchronized (tuple) {
|
||||||
info[0] = true; // Done
|
tuple.complete = true; // Done
|
||||||
info[1] = pollRes; // Polling result
|
tuple.returnedDetails = details; // Polling result
|
||||||
|
|
||||||
info.notify();
|
tuple.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Fast Poll - "+address);
|
tuple.pollingThread.setName("Parallel Poll - "+tuple.address+" - "+tuple.existingDetails.name);
|
||||||
t.start();
|
tuple.pollingThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress, final String ipv6Address) throws InterruptedException {
|
private ComputerDetails parallelPollPc(ComputerDetails details) throws InterruptedException {
|
||||||
final boolean[] remoteInfo = new boolean[2];
|
ParallelPollTuple localInfo = new ParallelPollTuple(details.localAddress, details);
|
||||||
final boolean[] localInfo = new boolean[2];
|
ParallelPollTuple manualInfo = new ParallelPollTuple(details.manualAddress, details);
|
||||||
final boolean[] manualInfo = new boolean[2];
|
ParallelPollTuple remoteInfo = new ParallelPollTuple(details.remoteAddress, details);
|
||||||
final boolean[] ipv6Info = new boolean[2];
|
ParallelPollTuple ipv6Info = new ParallelPollTuple(details.ipv6Address, details);
|
||||||
|
|
||||||
startFastPollThread(localAddress, localInfo);
|
// These must be started in order of precedence for the deduplication algorithm
|
||||||
startFastPollThread(remoteAddress, remoteInfo);
|
// to result in the correct behavior.
|
||||||
startFastPollThread(manualAddress, manualInfo);
|
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||||
startFastPollThread(ipv6Address, ipv6Info);
|
startParallelPollThread(localInfo, uniqueAddresses);
|
||||||
|
startParallelPollThread(manualInfo, uniqueAddresses);
|
||||||
|
startParallelPollThread(remoteInfo, uniqueAddresses);
|
||||||
|
startParallelPollThread(ipv6Info, uniqueAddresses);
|
||||||
|
|
||||||
// Check local first
|
try {
|
||||||
synchronized (localInfo) {
|
// Check local first
|
||||||
while (!localInfo[0]) {
|
synchronized (localInfo) {
|
||||||
localInfo.wait(500);
|
while (!localInfo.complete) {
|
||||||
|
localInfo.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localInfo.returnedDetails != null) {
|
||||||
|
localInfo.returnedDetails.activeAddress = localInfo.address;
|
||||||
|
return localInfo.returnedDetails;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localInfo[1]) {
|
// Now manual
|
||||||
return localAddress;
|
synchronized (manualInfo) {
|
||||||
}
|
while (!manualInfo.complete) {
|
||||||
}
|
manualInfo.wait();
|
||||||
|
}
|
||||||
|
|
||||||
// Now manual
|
if (manualInfo.returnedDetails != null) {
|
||||||
synchronized (manualInfo) {
|
manualInfo.returnedDetails.activeAddress = manualInfo.address;
|
||||||
while (!manualInfo[0]) {
|
return manualInfo.returnedDetails;
|
||||||
manualInfo.wait(500);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manualInfo[1]) {
|
// Now remote IPv4
|
||||||
return manualAddress;
|
synchronized (remoteInfo) {
|
||||||
}
|
while (!remoteInfo.complete) {
|
||||||
}
|
remoteInfo.wait();
|
||||||
|
}
|
||||||
|
|
||||||
// Now remote IPv4
|
if (remoteInfo.returnedDetails != null) {
|
||||||
synchronized (remoteInfo) {
|
remoteInfo.returnedDetails.activeAddress = remoteInfo.address;
|
||||||
while (!remoteInfo[0]) {
|
return remoteInfo.returnedDetails;
|
||||||
remoteInfo.wait(500);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteInfo[1]) {
|
// Now global IPv6
|
||||||
return remoteAddress;
|
synchronized (ipv6Info) {
|
||||||
}
|
while (!ipv6Info.complete) {
|
||||||
}
|
ipv6Info.wait();
|
||||||
|
}
|
||||||
|
|
||||||
// Now global IPv6
|
if (ipv6Info.returnedDetails != null) {
|
||||||
synchronized (ipv6Info) {
|
ipv6Info.returnedDetails.activeAddress = ipv6Info.address;
|
||||||
while (!ipv6Info[0]) {
|
return ipv6Info.returnedDetails;
|
||||||
ipv6Info.wait(500);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (ipv6Info[1]) {
|
|
||||||
return ipv6Address;
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// Stop any further polling if we've found a working address or we've been
|
||||||
|
// interrupted by an attempt to stop polling.
|
||||||
|
localInfo.interrupt();
|
||||||
|
manualInfo.interrupt();
|
||||||
|
remoteInfo.interrupt();
|
||||||
|
ipv6Info.interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||||
ComputerDetails polledDetails;
|
// Poll all addresses in parallel to speed up the process
|
||||||
|
LimeLog.info("Starting parallel poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening.
|
ComputerDetails polledDetails = parallelPollPc(details);
|
||||||
// Do not write this address to details.activeAddress because:
|
LimeLog.info("Parallel poll for "+details.name+" returned address: "+details.activeAddress);
|
||||||
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
|
|
||||||
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
|
|
||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
|
|
||||||
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address);
|
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
|
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
|
||||||
if (candidateAddress == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try using the active address from fast-poll
|
|
||||||
polledDetails = tryPollIp(details, candidateAddress);
|
|
||||||
if (polledDetails == null) {
|
|
||||||
// If that failed, try all unique addresses except what we've
|
|
||||||
// already tried
|
|
||||||
HashSet<String> uniqueAddresses = new HashSet<>();
|
|
||||||
uniqueAddresses.add(details.localAddress);
|
|
||||||
uniqueAddresses.add(details.manualAddress);
|
|
||||||
uniqueAddresses.add(details.remoteAddress);
|
|
||||||
uniqueAddresses.add(details.ipv6Address);
|
|
||||||
for (String addr : uniqueAddresses) {
|
|
||||||
if (addr == null || addr.equals(candidateAddress)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
polledDetails = tryPollIp(details, addr);
|
|
||||||
if (polledDetails != null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polledDetails != null) {
|
if (polledDetails != null) {
|
||||||
details.update(polledDetails);
|
details.update(polledDetails);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.limelight.grid;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -16,8 +17,12 @@ import com.limelight.grid.assets.NetworkAssetLoader;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
@@ -27,23 +32,49 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
|
|
||||||
private final ComputerDetails computer;
|
private final ComputerDetails computer;
|
||||||
private final String uniqueId;
|
private final String uniqueId;
|
||||||
|
private final boolean showHiddenApps;
|
||||||
|
|
||||||
private CachedAppAssetLoader loader;
|
private CachedAppAssetLoader loader;
|
||||||
|
private Set<Integer> hiddenAppIds = new HashSet<>();
|
||||||
|
private ArrayList<AppView.AppObject> allApps = new ArrayList<>();
|
||||||
|
|
||||||
public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId) {
|
public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId, boolean showHiddenApps) {
|
||||||
super(context, getLayoutIdForPreferences(prefs));
|
super(context, getLayoutIdForPreferences(prefs));
|
||||||
|
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.uniqueId = uniqueId;
|
this.uniqueId = uniqueId;
|
||||||
|
this.showHiddenApps = showHiddenApps;
|
||||||
|
|
||||||
updateLayoutWithPreferences(context, prefs);
|
updateLayoutWithPreferences(context, prefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
public void updateHiddenApps(Set<Integer> newHiddenAppIds, boolean hideImmediately) {
|
||||||
if (prefs.listMode) {
|
this.hiddenAppIds.clear();
|
||||||
return R.layout.simple_row;
|
this.hiddenAppIds.addAll(newHiddenAppIds);
|
||||||
|
|
||||||
|
if (hideImmediately) {
|
||||||
|
// Reconstruct the itemList with the new hidden app set
|
||||||
|
itemList.clear();
|
||||||
|
for (AppView.AppObject app : allApps) {
|
||||||
|
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
|
||||||
|
|
||||||
|
if (!app.isHidden || showHiddenApps) {
|
||||||
|
itemList.add(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (prefs.smallIconMode) {
|
else {
|
||||||
|
// Just update the isHidden state to show the correct UI indication
|
||||||
|
for (AppView.AppObject app : allApps) {
|
||||||
|
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
||||||
|
if (prefs.smallIconMode) {
|
||||||
return R.layout.app_grid_item_small;
|
return R.layout.app_grid_item_small;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -90,8 +121,8 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
loader.freeCacheMemory();
|
loader.freeCacheMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sortList() {
|
private static void sortList(List<AppView.AppObject> list) {
|
||||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
Collections.sort(list, 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().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
||||||
@@ -100,43 +131,54 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addApp(AppView.AppObject app) {
|
public void addApp(AppView.AppObject app) {
|
||||||
// Queue a request to fetch this bitmap into cache
|
// Update hidden state
|
||||||
loader.queueCacheLoad(app.app);
|
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
|
||||||
|
|
||||||
// Add the app to our sorted list
|
// Always add the app to the all apps list
|
||||||
itemList.add(app);
|
allApps.add(app);
|
||||||
sortList();
|
sortList(allApps);
|
||||||
|
|
||||||
|
// Add the app to the adapter data if it's not hidden
|
||||||
|
if (showHiddenApps || !app.isHidden) {
|
||||||
|
// Queue a request to fetch this bitmap into cache
|
||||||
|
loader.queueCacheLoad(app.app);
|
||||||
|
|
||||||
|
// Add the app to our sorted list
|
||||||
|
itemList.add(app);
|
||||||
|
sortList(itemList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeApp(AppView.AppObject app) {
|
public void removeApp(AppView.AppObject app) {
|
||||||
itemList.remove(app);
|
itemList.remove(app);
|
||||||
|
allApps.remove(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
public void clear() {
|
||||||
|
super.clear();
|
||||||
|
allApps.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, AppView.AppObject obj) {
|
||||||
// Let the cached asset loader handle it
|
// Let the cached asset loader handle it
|
||||||
loader.populateImageView(obj.app, imgView, prgView);
|
loader.populateImageView(obj.app, imgView, txtView);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
|
|
||||||
// Select the text view so it starts marquee mode
|
|
||||||
txtView.setSelected(true);
|
|
||||||
|
|
||||||
// Return false to use the app's toString method
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
|
||||||
if (obj.isRunning) {
|
if (obj.isRunning) {
|
||||||
// Show the play button overlay
|
// Show the play button overlay
|
||||||
overlayView.setImageResource(R.drawable.ic_play);
|
overlayView.setImageResource(R.drawable.ic_play);
|
||||||
return true;
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlayView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No overlay
|
if (obj.isHidden) {
|
||||||
return false;
|
parentView.setAlpha(0.40f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parentView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import android.widget.ProgressBar;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@@ -55,9 +54,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
|
public abstract void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, T obj);
|
||||||
public abstract boolean populateTextView(TextView txtView, T obj);
|
|
||||||
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
||||||
@@ -70,22 +67,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
TextView txtView = convertView.findViewById(R.id.grid_text);
|
TextView txtView = convertView.findViewById(R.id.grid_text);
|
||||||
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
||||||
|
|
||||||
if (imgView != null) {
|
populateView(convertView, imgView, prgView, txtView, overlayView, itemList.get(i));
|
||||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
|
||||||
imgView.setImageBitmap(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!populateTextView(txtView, itemList.get(i))) {
|
|
||||||
txtView.setText(itemList.get(i).toString());
|
|
||||||
}
|
|
||||||
if (overlayView != null) {
|
|
||||||
if (!populateOverlayView(overlayView, itemList.get(i))) {
|
|
||||||
overlayView.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
overlayView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertView;
|
return convertView;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
||||||
if (prefs.listMode) {
|
return R.layout.pc_grid_item;
|
||||||
return R.layout.simple_row;
|
|
||||||
}
|
|
||||||
else if (prefs.smallIconMode) {
|
|
||||||
return R.layout.pc_grid_item_small;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return R.layout.pc_grid_item;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
|
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
|
||||||
@@ -57,7 +49,8 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
public void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, PcView.ComputerObject obj) {
|
||||||
|
imgView.setImageResource(R.drawable.ic_computer);
|
||||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||||
imgView.setAlpha(1.0f);
|
imgView.setAlpha(1.0f);
|
||||||
}
|
}
|
||||||
@@ -72,12 +65,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
prgView.setVisibility(View.INVISIBLE);
|
prgView.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
imgView.setImageResource(R.drawable.ic_computer);
|
txtView.setText(obj.details.name);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
|
||||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||||
txtView.setAlpha(1.0f);
|
txtView.setAlpha(1.0f);
|
||||||
}
|
}
|
||||||
@@ -85,16 +73,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
txtView.setAlpha(0.4f);
|
txtView.setAlpha(0.4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return false to use the computer's toString method
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
|
||||||
if (obj.details.state == ComputerDetails.State.OFFLINE) {
|
if (obj.details.state == ComputerDetails.State.OFFLINE) {
|
||||||
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
||||||
overlayView.setAlpha(0.4f);
|
overlayView.setAlpha(0.4f);
|
||||||
return true;
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
// We must check if the status is exactly online and unpaired
|
// We must check if the status is exactly online and unpaired
|
||||||
// to avoid colliding with the loading spinner when status is unknown
|
// to avoid colliding with the loading spinner when status is unknown
|
||||||
@@ -102,8 +84,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
|
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
|
||||||
overlayView.setImageResource(R.drawable.ic_lock);
|
overlayView.setImageResource(R.drawable.ic_lock);
|
||||||
overlayView.setAlpha(1.0f);
|
overlayView.setAlpha(1.0f);
|
||||||
return true;
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlayView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import android.graphics.drawable.BitmapDrawable;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
@@ -89,7 +92,7 @@ public class CachedAppAssetLoader {
|
|||||||
memoryLoader.clearCache();
|
memoryLoader.clearCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
private ScaledBitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
||||||
// Try 3 times
|
// Try 3 times
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
// Check again whether we've been cancelled or the image view is gone
|
// Check again whether we've been cancelled or the image view is gone
|
||||||
@@ -110,7 +113,7 @@ public class CachedAppAssetLoader {
|
|||||||
// If there's a task associated with this load, we should return the bitmap
|
// If there's a task associated with this load, we should return the bitmap
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
return bmp;
|
return bmp;
|
||||||
}
|
}
|
||||||
@@ -125,6 +128,13 @@ public class CachedAppAssetLoader {
|
|||||||
try {
|
try {
|
||||||
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,29 +142,29 @@ public class CachedAppAssetLoader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
private class LoaderTask extends AsyncTask<LoaderTuple, Void, ScaledBitmap> {
|
||||||
private final WeakReference<ImageView> imageViewRef;
|
private final WeakReference<ImageView> imageViewRef;
|
||||||
private final WeakReference<ProgressBar> progressViewRef;
|
private final WeakReference<TextView> textViewRef;
|
||||||
private final boolean diskOnly;
|
private final boolean diskOnly;
|
||||||
|
|
||||||
private LoaderTuple tuple;
|
private LoaderTuple tuple;
|
||||||
|
|
||||||
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
public LoaderTask(ImageView imageView, TextView textView, boolean diskOnly) {
|
||||||
this.imageViewRef = new WeakReference<>(imageView);
|
this.imageViewRef = new WeakReference<>(imageView);
|
||||||
this.progressViewRef = new WeakReference<>(prgView);
|
this.textViewRef = new WeakReference<>(textView);
|
||||||
this.diskOnly = diskOnly;
|
this.diskOnly = diskOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
protected ScaledBitmap doInBackground(LoaderTuple... params) {
|
||||||
tuple = params[0];
|
tuple = params[0];
|
||||||
|
|
||||||
// Check whether it has been cancelled or the views are gone
|
// Check whether it has been cancelled or the views are gone
|
||||||
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
if (isCancelled() || imageViewRef.get() == null || textViewRef.get() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
if (bmp == null) {
|
if (bmp == null) {
|
||||||
if (!diskOnly) {
|
if (!diskOnly) {
|
||||||
// Try to load the asset from the network
|
// Try to load the asset from the network
|
||||||
@@ -183,45 +193,61 @@ 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();
|
final TextView textView = textViewRef.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. This time our AsyncDrawable
|
// Set off another loader task on the network executor. This time our AsyncDrawable
|
||||||
// will use the app image placeholder bitmap, rather than an empty bitmap.
|
// will use the app image placeholder bitmap, rather than an empty bitmap.
|
||||||
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
LoaderTask task = new LoaderTask(imageView, textView, false);
|
||||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
|
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
|
||||||
imageView.setVisibility(View.VISIBLE);
|
|
||||||
imageView.setImageDrawable(asyncDrawable);
|
imageView.setImageDrawable(asyncDrawable);
|
||||||
|
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
textView.setVisibility(View.VISIBLE);
|
||||||
task.executeOnExecutor(networkExecutor, tuple);
|
task.executeOnExecutor(networkExecutor, tuple);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Bitmap bitmap) {
|
protected void onPostExecute(final ScaledBitmap bitmap) {
|
||||||
// Do nothing if cancelled
|
// Do nothing if cancelled
|
||||||
if (isCancelled()) {
|
if (isCancelled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
final ImageView imageView = imageViewRef.get();
|
||||||
final ProgressBar prgView = progressViewRef.get();
|
final TextView textView = textViewRef.get();
|
||||||
if (getLoaderTask(imageView) == this) {
|
if (getLoaderTask(imageView) == this) {
|
||||||
// Set the bitmap
|
// Fade in the box art
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
imageView.setImageBitmap(bitmap);
|
// Show the text if it's a placeholder
|
||||||
}
|
textView.setVisibility(isBitmapPlaceholder(bitmap) ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
// Hide the progress bar
|
if (imageView.getVisibility() == View.VISIBLE) {
|
||||||
if (prgView != null) {
|
// Fade out the placeholder first
|
||||||
prgView.setVisibility(View.INVISIBLE);
|
Animation fadeOutAnimation = AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadeout);
|
||||||
}
|
fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animation animation) {}
|
||||||
|
|
||||||
// Show the view
|
@Override
|
||||||
imageView.setVisibility(View.VISIBLE);
|
public void onAnimationEnd(Animation animation) {
|
||||||
|
// Fade in the new box art
|
||||||
|
imageView.setImageBitmap(bitmap.bitmap);
|
||||||
|
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(Animation animation) {}
|
||||||
|
});
|
||||||
|
imageView.startAnimation(fadeOutAnimation);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// View is invisible already, so just fade in the new art
|
||||||
|
imageView.setImageBitmap(bitmap.bitmap);
|
||||||
|
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,7 +325,13 @@ public class CachedAppAssetLoader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
|
private boolean isBitmapPlaceholder(ScaledBitmap bitmap) {
|
||||||
|
return (bitmap == null) ||
|
||||||
|
(bitmap.originalWidth == 130 && bitmap.originalHeight == 180) || // GFE 2.0
|
||||||
|
(bitmap.originalWidth == 628 && bitmap.originalHeight == 888); // GFE 3.0
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean populateImageView(NvApp app, ImageView imgView, TextView textView) {
|
||||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
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,
|
||||||
@@ -309,22 +341,26 @@ public class CachedAppAssetLoader {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the progress bar always on initial load
|
// Always set the name text so we have it if needed later
|
||||||
prgView.setVisibility(View.INVISIBLE);
|
textView.setText(app.getAppName());
|
||||||
|
|
||||||
// First, try the memory cache in the current context
|
// First, try the memory cache in the current context
|
||||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
ScaledBitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
// Show the bitmap immediately
|
// Show the bitmap immediately
|
||||||
imgView.setVisibility(View.VISIBLE);
|
imgView.setVisibility(View.VISIBLE);
|
||||||
imgView.setImageBitmap(bmp);
|
imgView.setImageBitmap(bmp.bitmap);
|
||||||
|
|
||||||
|
// Show the text if it's a placeholder bitmap
|
||||||
|
textView.setVisibility(isBitmapPlaceholder(bmp) ? View.VISIBLE : View.GONE);
|
||||||
return true;
|
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(imgView, prgView, true);
|
final LoaderTask task = new LoaderTask(imgView, textView, true);
|
||||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
||||||
|
textView.setVisibility(View.INVISIBLE);
|
||||||
imgView.setVisibility(View.INVISIBLE);
|
imgView.setVisibility(View.INVISIBLE);
|
||||||
imgView.setImageDrawable(asyncDrawable);
|
imgView.setImageDrawable(asyncDrawable);
|
||||||
|
|
||||||
@@ -333,7 +369,7 @@ public class CachedAppAssetLoader {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LoaderTuple {
|
public static class LoaderTuple {
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
public final NvApp app;
|
public final NvApp app;
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class DiskAssetLoader {
|
|||||||
return inSampleSize;
|
return inSampleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
||||||
|
|
||||||
// Don't bother with anything if it doesn't exist
|
// Don't bother with anything if it doesn't exist
|
||||||
@@ -110,27 +110,33 @@ public class DiskAssetLoader {
|
|||||||
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
||||||
|
return new ScaledBitmap(decodeOnlyOptions.outWidth, decodeOnlyOptions.outHeight, bmp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// On P, we can get a bitmap back in one step with ImageDecoder
|
// On P, we can get a bitmap back in one step with ImageDecoder
|
||||||
|
final ScaledBitmap scaledBitmap = new ScaledBitmap();
|
||||||
try {
|
try {
|
||||||
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
scaledBitmap.bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
||||||
|
scaledBitmap.originalWidth = imageInfo.getSize().getWidth();
|
||||||
|
scaledBitmap.originalHeight = imageInfo.getSize().getHeight();
|
||||||
|
|
||||||
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
||||||
if (isLowRamDevice) {
|
if (isLowRamDevice) {
|
||||||
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return scaledBitmap;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bmp;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getFile(String computerUuid, int appId) {
|
public File getFile(String computerUuid, int appId) {
|
||||||
|
|||||||
@@ -1,37 +1,74 @@
|
|||||||
package com.limelight.grid.assets;
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.util.LruCache;
|
import android.util.LruCache;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
public class MemoryAssetLoader {
|
public class MemoryAssetLoader {
|
||||||
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
||||||
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
private static final LruCache<String, ScaledBitmap> memoryCache = new LruCache<String, ScaledBitmap>(maxMemory / 16) {
|
||||||
@Override
|
@Override
|
||||||
protected int sizeOf(String key, Bitmap bitmap) {
|
protected int sizeOf(String key, ScaledBitmap bitmap) {
|
||||||
// Sizeof returns kilobytes
|
// Sizeof returns kilobytes
|
||||||
return bitmap.getByteCount() / 1024;
|
return bitmap.bitmap.getByteCount() / 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void entryRemoved(boolean evicted, String key, ScaledBitmap oldValue, ScaledBitmap newValue) {
|
||||||
|
super.entryRemoved(evicted, key, oldValue, newValue);
|
||||||
|
|
||||||
|
if (evicted) {
|
||||||
|
// Keep a soft reference around to the bitmap as long as we can
|
||||||
|
evictionCache.put(key, new SoftReference<>(oldValue));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
private static final HashMap<String, SoftReference<ScaledBitmap>> evictionCache = new HashMap<>();
|
||||||
|
|
||||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
final String key = constructKey(tuple);
|
||||||
|
|
||||||
|
ScaledBitmap bmp = memoryCache.get(key);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
LimeLog.info("LRU cache hit for tuple: "+tuple);
|
||||||
|
return bmp;
|
||||||
}
|
}
|
||||||
return bmp;
|
|
||||||
|
SoftReference<ScaledBitmap> bmpRef = evictionCache.get(key);
|
||||||
|
if (bmpRef != null) {
|
||||||
|
bmp = bmpRef.get();
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Eviction cache hit for tuple: "+tuple);
|
||||||
|
|
||||||
|
// Put this entry back into the LRU cache
|
||||||
|
evictionCache.remove(key);
|
||||||
|
memoryCache.put(key, bmp);
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The data is gone, so remove the dangling SoftReference now
|
||||||
|
evictionCache.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, ScaledBitmap bitmap) {
|
||||||
memoryCache.put(constructKey(tuple), bitmap);
|
memoryCache.put(constructKey(tuple), bitmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
|
// We must evict first because that will push all items into the eviction cache
|
||||||
memoryCache.evictAll();
|
memoryCache.evictAll();
|
||||||
|
evictionCache.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
public class ScaledBitmap {
|
||||||
|
public int originalWidth;
|
||||||
|
public int originalHeight;
|
||||||
|
|
||||||
|
public Bitmap bitmap;
|
||||||
|
|
||||||
|
public ScaledBitmap() {}
|
||||||
|
|
||||||
|
public ScaledBitmap(int originalWidth, int originalHeight, Bitmap bitmap) {
|
||||||
|
this.originalWidth = originalWidth;
|
||||||
|
this.originalHeight = originalHeight;
|
||||||
|
this.bitmap = bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ public class ConnectionContext {
|
|||||||
// This is the version quad from the appversion tag of /serverinfo
|
// This is the version quad from the appversion tag of /serverinfo
|
||||||
public String serverAppVersion;
|
public String serverAppVersion;
|
||||||
public String serverGfeVersion;
|
public String serverGfeVersion;
|
||||||
|
|
||||||
|
// This is the sessionUrl0 tag from /resume and /launch
|
||||||
|
public String rtspSessionUrl;
|
||||||
|
|
||||||
public int negotiatedWidth, negotiatedHeight;
|
public int negotiatedWidth, negotiatedHeight;
|
||||||
public boolean negotiatedHdr;
|
public boolean negotiatedHdr;
|
||||||
|
|||||||
@@ -43,25 +43,26 @@ public class NvConnection {
|
|||||||
this.context = new ConnectionContext();
|
this.context = new ConnectionContext();
|
||||||
this.context.streamConfig = config;
|
this.context.streamConfig = config;
|
||||||
this.context.serverCert = serverCert;
|
this.context.serverCert = serverCert;
|
||||||
try {
|
|
||||||
// This is unique per connection
|
// This is unique per connection
|
||||||
this.context.riKey = generateRiAesKey();
|
this.context.riKey = generateRiAesKey();
|
||||||
} catch (NoSuchAlgorithmException e) {
|
context.riKeyId = generateRiKeyId();
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context.riKeyId = generateRiKeyId();
|
|
||||||
this.isMonkey = ActivityManager.isUserAMonkey();
|
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
|
private static SecretKey generateRiAesKey() {
|
||||||
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
try {
|
||||||
|
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||||
// RI keys are 128 bits
|
|
||||||
keyGen.init(128);
|
// RI keys are 128 bits
|
||||||
|
keyGen.init(128);
|
||||||
return keyGen.generateKey();
|
|
||||||
|
return keyGen.generateKey();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int generateRiKeyId() {
|
private static int generateRiKeyId() {
|
||||||
@@ -114,7 +115,17 @@ public class NvConnection {
|
|||||||
//
|
//
|
||||||
|
|
||||||
// Check for a supported stream resolution
|
// Check for a supported stream resolution
|
||||||
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
if ((context.streamConfig.getWidth() > 4096 || context.streamConfig.getHeight() > 4096) &&
|
||||||
|
(h.getServerCodecModeSupport(serverInfo) & 0x200) == 0) {
|
||||||
|
context.connListener.displayMessage("Your host PC does not support streaming at resolutions above 4K.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if ((context.streamConfig.getWidth() > 4096 || context.streamConfig.getHeight() > 4096) &&
|
||||||
|
!context.streamConfig.getHevcSupported()) {
|
||||||
|
context.connListener.displayMessage("Your streaming device must support HEVC to stream at resolutions above 4K.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
||||||
// Client wants 4K but the server can't do it
|
// Client wants 4K but the server can't do it
|
||||||
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
||||||
|
|
||||||
@@ -230,18 +241,19 @@ public class NvConnection {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!startApp()) {
|
if (!startApp()) {
|
||||||
context.connListener.stageFailed(appName, 0);
|
context.connListener.stageFailed(appName, 0, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.connListener.stageComplete(appName);
|
context.connListener.stageComplete(appName);
|
||||||
} catch (GfeHttpResponseException e) {
|
} catch (GfeHttpResponseException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
context.connListener.displayMessage(e.getMessage());
|
context.connListener.displayMessage(e.getMessage());
|
||||||
context.connListener.stageFailed(appName, e.getErrorCode());
|
context.connListener.stageFailed(appName, 0, e.getErrorCode());
|
||||||
|
return;
|
||||||
} catch (XmlPullParserException | IOException e) {
|
} catch (XmlPullParserException | IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
context.connListener.displayMessage(e.getMessage());
|
context.connListener.displayMessage(e.getMessage());
|
||||||
context.connListener.stageFailed(appName, 0);
|
context.connListener.stageFailed(appName, MoonBridge.ML_PORT_FLAG_TCP_47984 | MoonBridge.ML_PORT_FLAG_TCP_47989, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +266,7 @@ public class NvConnection {
|
|||||||
connectionAllowed.acquire();
|
connectionAllowed.acquire();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
context.connListener.displayMessage(e.getMessage());
|
context.connListener.displayMessage(e.getMessage());
|
||||||
context.connListener.stageFailed(appName, 0);
|
context.connListener.stageFailed(appName, 0, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +275,7 @@ public class NvConnection {
|
|||||||
synchronized (MoonBridge.class) {
|
synchronized (MoonBridge.class) {
|
||||||
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||||
int ret = MoonBridge.startConnection(context.serverAddress,
|
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||||
context.serverAppVersion, context.serverGfeVersion,
|
context.serverAppVersion, context.serverGfeVersion, context.rtspSessionUrl,
|
||||||
context.negotiatedWidth, context.negotiatedHeight,
|
context.negotiatedWidth, context.negotiatedHeight,
|
||||||
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
||||||
context.streamConfig.getMaxPacketSize(),
|
context.streamConfig.getMaxPacketSize(),
|
||||||
@@ -272,6 +284,7 @@ public class NvConnection {
|
|||||||
context.negotiatedHdr,
|
context.negotiatedHdr,
|
||||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||||
context.streamConfig.getClientRefreshRateX100(),
|
context.streamConfig.getClientRefreshRateX100(),
|
||||||
|
context.streamConfig.getEncryptionFlags(),
|
||||||
context.riKey.getEncoded(), ib.array(),
|
context.riKey.getEncoded(), ib.array(),
|
||||||
context.videoCapabilities);
|
context.videoCapabilities);
|
||||||
if (ret != 0) {
|
if (ret != 0) {
|
||||||
@@ -298,7 +311,14 @@ public class NvConnection {
|
|||||||
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
|
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendMouseMoveAsMousePosition(short deltaX, short deltaY, short referenceWidth, short referenceHeight)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseMoveAsMousePosition(deltaX, deltaY, referenceWidth, referenceHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void sendMouseButtonDown(final byte mouseButton)
|
public void sendMouseButtonDown(final byte mouseButton)
|
||||||
{
|
{
|
||||||
if (!isMonkey) {
|
if (!isMonkey) {
|
||||||
@@ -348,6 +368,18 @@ public class NvConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendMouseHighResScroll(final short scrollAmount) {
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseHighResScroll(scrollAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendUtf8Text(final String text) {
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendUtf8Text(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
||||||
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.limelight.nvstream;
|
|||||||
public interface NvConnectionListener {
|
public interface NvConnectionListener {
|
||||||
void stageStarting(String stage);
|
void stageStarting(String stage);
|
||||||
void stageComplete(String stage);
|
void stageComplete(String stage);
|
||||||
void stageFailed(String stage, int errorCode);
|
void stageFailed(String stage, int portFlags, int errorCode);
|
||||||
|
|
||||||
void connectionStarted();
|
void connectionStarted();
|
||||||
void connectionTerminated(int errorCode);
|
void connectionTerminated(int errorCode);
|
||||||
@@ -13,4 +13,6 @@ public interface NvConnectionListener {
|
|||||||
void displayTransientMessage(String message);
|
void displayTransientMessage(String message);
|
||||||
|
|
||||||
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||||
|
|
||||||
|
void setHdrMode(boolean enabled);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class StreamConfiguration {
|
|||||||
private int hevcBitratePercentageMultiplier;
|
private int hevcBitratePercentageMultiplier;
|
||||||
private boolean enableHdr;
|
private boolean enableHdr;
|
||||||
private int attachedGamepadMask;
|
private int attachedGamepadMask;
|
||||||
|
private int encryptionFlags;
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private StreamConfiguration config = new StreamConfiguration();
|
private StreamConfiguration config = new StreamConfiguration();
|
||||||
@@ -110,7 +111,17 @@ public class StreamConfiguration {
|
|||||||
config.clientRefreshRateX100 = refreshRateX100;
|
config.clientRefreshRateX100 = refreshRateX100;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setAudioEncryption(boolean enable) {
|
||||||
|
if (enable) {
|
||||||
|
config.encryptionFlags |= MoonBridge.ENCFLG_AUDIO;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
config.encryptionFlags &= ~MoonBridge.ENCFLG_AUDIO;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
|
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
|
||||||
config.audioConfiguration = audioConfig;
|
config.audioConfiguration = audioConfig;
|
||||||
return this;
|
return this;
|
||||||
@@ -211,4 +222,8 @@ public class StreamConfiguration {
|
|||||||
public int getClientRefreshRateX100() {
|
public int getClientRefreshRateX100() {
|
||||||
return clientRefreshRateX100;
|
return clientRefreshRateX100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getEncryptionFlags() {
|
||||||
|
return encryptionFlags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ public abstract class VideoDecoderRenderer {
|
|||||||
// This is called once for each frame-start NALU. This means it will be called several times
|
// This is called once for each frame-start NALU. This means it will be called several times
|
||||||
// for an IDR frame which contains several parameter sets and the I-frame data.
|
// for an IDR frame which contains several parameter sets and the I-frame data.
|
||||||
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||||
int frameNumber, long receiveTimeMs);
|
int frameNumber, int frameType, long receiveTimeMs, long enqueueTimeMs);
|
||||||
|
|
||||||
public abstract void cleanup();
|
public abstract void cleanup();
|
||||||
|
|
||||||
public abstract int getCapabilities();
|
public abstract int getCapabilities();
|
||||||
|
|
||||||
|
public abstract void setHdrMode(boolean enabled);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ public class ComputerDetails {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder str = new StringBuilder();
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append("Name: ").append(name).append("\n");
|
||||||
str.append("State: ").append(state).append("\n");
|
str.append("State: ").append(state).append("\n");
|
||||||
str.append("Active Address: ").append(activeAddress).append("\n");
|
str.append("Active Address: ").append(activeAddress).append("\n");
|
||||||
str.append("Name: ").append(name).append("\n");
|
|
||||||
str.append("UUID: ").append(uuid).append("\n");
|
str.append("UUID: ").append(uuid).append("\n");
|
||||||
str.append("Local Address: ").append(localAddress).append("\n");
|
str.append("Local Address: ").append(localAddress).append("\n");
|
||||||
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
||||||
|
|||||||
@@ -58,4 +58,13 @@ public class NvApp {
|
|||||||
public boolean isInitialized() {
|
public boolean isInitialized() {
|
||||||
return this.initialized;
|
return this.initialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append("Name: ").append(appName).append("\n");
|
||||||
|
str.append("HDR: ").append(hdrSupported ? "Yes" : "No").append("\n");
|
||||||
|
str.append("ID: ").append(appId).append("\n");
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
package com.limelight.nvstream.http;
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.MalformedURLException;
|
import java.net.InetAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -24,11 +27,16 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
import javax.net.ssl.KeyManager;
|
import javax.net.ssl.KeyManager;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.SSLHandshakeException;
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
import javax.net.ssl.SSLSession;
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
import javax.net.ssl.X509KeyManager;
|
import javax.net.ssl.X509KeyManager;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
@@ -42,7 +50,7 @@ import com.limelight.nvstream.ConnectionContext;
|
|||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
|
|
||||||
import okhttp3.ConnectionPool;
|
import okhttp3.ConnectionPool;
|
||||||
import okhttp3.Handshake;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
@@ -61,39 +69,41 @@ public class NvHTTP {
|
|||||||
// Print URL and content to logcat on debug builds
|
// Print URL and content to logcat on debug builds
|
||||||
private static boolean verbose = BuildConfig.DEBUG;
|
private static boolean verbose = BuildConfig.DEBUG;
|
||||||
|
|
||||||
public String baseUrlHttps;
|
private HttpUrl baseUrlHttps;
|
||||||
public String baseUrlHttp;
|
private HttpUrl baseUrlHttp;
|
||||||
|
|
||||||
private OkHttpClient httpClient;
|
private OkHttpClient httpClient;
|
||||||
private OkHttpClient httpClientWithReadTimeout;
|
private OkHttpClient httpClientWithReadTimeout;
|
||||||
|
|
||||||
|
private X509TrustManager defaultTrustManager;
|
||||||
private X509TrustManager trustManager;
|
private X509TrustManager trustManager;
|
||||||
private X509KeyManager keyManager;
|
private X509KeyManager keyManager;
|
||||||
private X509Certificate serverCert;
|
private X509Certificate serverCert;
|
||||||
|
|
||||||
void setServerCert(X509Certificate serverCert) {
|
void setServerCert(X509Certificate serverCert) {
|
||||||
this.serverCert = serverCert;
|
this.serverCert = serverCert;
|
||||||
|
|
||||||
trustManager = new X509TrustManager() {
|
|
||||||
public X509Certificate[] getAcceptedIssuers() {
|
|
||||||
return new X509Certificate[0];
|
|
||||||
}
|
|
||||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
|
||||||
throw new IllegalStateException("Should never be called");
|
|
||||||
}
|
|
||||||
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
|
||||||
// Check the server certificate if we've paired to this host
|
|
||||||
if (!certs[0].equals(NvHTTP.this.serverCert)) {
|
|
||||||
throw new CertificateException("Certificate mismatch");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) {
|
private static X509TrustManager getDefaultTrustManager() {
|
||||||
// Set up TrustManager
|
try {
|
||||||
setServerCert(serverCert);
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
tmf.init((KeyStore) null);
|
||||||
|
|
||||||
|
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||||
|
if (tm instanceof X509TrustManager) {
|
||||||
|
return (X509TrustManager) tm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (KeyStoreException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("No X509 trust manager found");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeHttpState(final LimelightCryptoProvider cryptoProvider) {
|
||||||
keyManager = new X509KeyManager() {
|
keyManager = new X509KeyManager() {
|
||||||
public String chooseClientAlias(String[] keyTypes,
|
public String chooseClientAlias(String[] keyTypes,
|
||||||
Principal[] issuers, Socket socket) { return "Limelight-RSA"; }
|
Principal[] issuers, Socket socket) { return "Limelight-RSA"; }
|
||||||
@@ -109,9 +119,51 @@ public class NvHTTP {
|
|||||||
public String[] getServerAliases(String keyType, Principal[] issuers) { return null; }
|
public String[] getServerAliases(String keyType, Principal[] issuers) { return null; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ignore differences between given hostname and certificate hostname
|
defaultTrustManager = getDefaultTrustManager();
|
||||||
|
trustManager = new X509TrustManager() {
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return new X509Certificate[0];
|
||||||
|
}
|
||||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||||
|
throw new IllegalStateException("Should never be called");
|
||||||
|
}
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||||
|
try {
|
||||||
|
// Try the default trust manager first to allow pairing with certificates
|
||||||
|
// that chain up to a trusted root CA. This will raise CertificateException
|
||||||
|
// if the certificate is not trusted (expected for GFE's self-signed certs).
|
||||||
|
defaultTrustManager.checkServerTrusted(certs, authType);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// Check the server certificate if we've paired to this host
|
||||||
|
if (certs.length == 1 && NvHTTP.this.serverCert != null) {
|
||||||
|
if (!certs[0].equals(NvHTTP.this.serverCert)) {
|
||||||
|
throw new CertificateException("Certificate mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The cert chain doesn't look like a self-signed cert or we don't have
|
||||||
|
// a certificate pinned, so re-throw the original validation error.
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
HostnameVerifier hv = new HostnameVerifier() {
|
HostnameVerifier hv = new HostnameVerifier() {
|
||||||
public boolean verify(String hostname, SSLSession session) { return true; }
|
public boolean verify(String hostname, SSLSession session) {
|
||||||
|
try {
|
||||||
|
Certificate[] certificates = session.getPeerCertificates();
|
||||||
|
if (certificates.length == 1 && certificates[0].equals(NvHTTP.this.serverCert)) {
|
||||||
|
// Allow any hostname if it's our pinned cert
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (SSLPeerUnverifiedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default HostnameVerifier for validating CA-issued certs
|
||||||
|
return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
httpClient = new OkHttpClient.Builder()
|
httpClient = new OkHttpClient.Builder()
|
||||||
@@ -131,25 +183,31 @@ public class NvHTTP {
|
|||||||
// started by other Moonlight clients.
|
// started by other Moonlight clients.
|
||||||
this.uniqueId = "0123456789ABCDEF";
|
this.uniqueId = "0123456789ABCDEF";
|
||||||
|
|
||||||
initializeHttpState(serverCert, cryptoProvider);
|
this.serverCert = serverCert;
|
||||||
|
|
||||||
|
initializeHttpState(cryptoProvider);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// The URI constructor takes care of escaping IPv6 literals
|
this.baseUrlHttp = new HttpUrl.Builder()
|
||||||
this.baseUrlHttps = new URI("https", null, address, HTTPS_PORT, null, null, null).toString();
|
.scheme("http")
|
||||||
this.baseUrlHttp = new URI("http", null, address, HTTP_PORT, null, null, null).toString();
|
.host(address)
|
||||||
} catch (URISyntaxException e) {
|
.port(HTTP_PORT)
|
||||||
// Encapsulate URISyntaxException into IOException for callers to handle more easily
|
.build();
|
||||||
|
|
||||||
|
this.baseUrlHttps = new HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host(address)
|
||||||
|
.port(HTTPS_PORT)
|
||||||
|
.build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// Encapsulate IllegalArgumentException into IOException for callers to handle more easily
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pm = new PairingManager(this, cryptoProvider);
|
this.pm = new PairingManager(this, cryptoProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
String buildUniqueIdUuidString() {
|
static String getXmlString(Reader r, String tagname, boolean throwIfMissing) throws XmlPullParserException, IOException {
|
||||||
return "uniqueid="+uniqueId+"&uuid="+UUID.randomUUID();
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException {
|
|
||||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||||
factory.setNamespaceAware(true);
|
factory.setNamespaceAware(true);
|
||||||
XmlPullParser xpp = factory.newPullParser();
|
XmlPullParser xpp = factory.newPullParser();
|
||||||
@@ -171,35 +229,43 @@ public class NvHTTP {
|
|||||||
break;
|
break;
|
||||||
case (XmlPullParser.TEXT):
|
case (XmlPullParser.TEXT):
|
||||||
if (currentTag.peek().equals(tagname)) {
|
if (currentTag.peek().equals(tagname)) {
|
||||||
return xpp.getText().trim();
|
return xpp.getText();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
eventType = xpp.next();
|
eventType = xpp.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (throwIfMissing) {
|
||||||
|
// We throw an XmlPullParserException here for ease of handling in all the various callers.
|
||||||
|
// We could also throw an IOException, but some callers expect those in cases where the
|
||||||
|
// host may not be reachable. We want to distinguish unreachable hosts vs. hosts that
|
||||||
|
// are returning garbage XML to us, so we use XmlPullParserException instead.
|
||||||
|
throw new XmlPullParserException("Missing mandatory field in host response: "+tagname);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException {
|
static String getXmlString(String str, String tagname, boolean throwIfMissing) throws XmlPullParserException, IOException {
|
||||||
return getXmlString(new StringReader(str), tagname);
|
return getXmlString(new StringReader(str), tagname, throwIfMissing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
|
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
|
||||||
String statusCodeText = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code");
|
// We use Long.parseLong() because in rare cases GFE can send back a status code of
|
||||||
if (statusCodeText == null) {
|
// 0xFFFFFFFF, which will cause Integer.parseInt() to throw a NumberFormatException due
|
||||||
throw new GfeHttpResponseException(418, "Status code is missing");
|
// to exceeding Integer.MAX_VALUE. We'll get the desired error code of -1 by just casting
|
||||||
}
|
// the resulting long into an int.
|
||||||
try {
|
int statusCode = (int)Long.parseLong(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
|
||||||
int statusCode = Integer.parseInt(statusCodeText);
|
if (statusCode != 200) {
|
||||||
if (statusCode != 200) {
|
String statusMsg = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message");
|
||||||
throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message"));
|
if (statusCode == -1 && "Invalid".equals(statusMsg)) {
|
||||||
|
// Special case handling an audio capture error which GFE doesn't
|
||||||
|
// provide any useful status message for.
|
||||||
|
statusCode = 418;
|
||||||
|
statusMsg = "Missing audio capture device. Reinstall GeForce Experience.";
|
||||||
}
|
}
|
||||||
}
|
throw new GfeHttpResponseException(statusCode, statusMsg);
|
||||||
catch (NumberFormatException e) {
|
|
||||||
// It seems like GFE 3.20.3.63 is returning garbage for status_code in rare cases.
|
|
||||||
// Surface this in a more friendly way rather than crashing.
|
|
||||||
throw new GfeHttpResponseException(418, "Status code is not a number: "+statusCodeText);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +282,7 @@ public class NvHTTP {
|
|||||||
if (serverCert != null) {
|
if (serverCert != null) {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true);
|
resp = openHttpConnectionToString(baseUrlHttps, "serverinfo", true);
|
||||||
} catch (SSLHandshakeException e) {
|
} catch (SSLHandshakeException e) {
|
||||||
// Detect if we failed due to a server cert mismatch
|
// Detect if we failed due to a server cert mismatch
|
||||||
if (e.getCause() instanceof CertificateException) {
|
if (e.getCause() instanceof CertificateException) {
|
||||||
@@ -236,7 +302,7 @@ public class NvHTTP {
|
|||||||
catch (GfeHttpResponseException e) {
|
catch (GfeHttpResponseException e) {
|
||||||
if (e.getErrorCode() == 401) {
|
if (e.getErrorCode() == 401) {
|
||||||
// Cert validation error - fall back to HTTP
|
// Cert validation error - fall back to HTTP
|
||||||
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
|
return openHttpConnectionToString(baseUrlHttp, "serverinfo", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's not a cert validation error, throw it
|
// If it's not a cert validation error, throw it
|
||||||
@@ -247,7 +313,7 @@ public class NvHTTP {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// No pinned cert, so use HTTP
|
// No pinned cert, so use HTTP
|
||||||
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
|
return openHttpConnectionToString(baseUrlHttp , "serverinfo", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,26 +321,22 @@ public class NvHTTP {
|
|||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
String serverInfo = getServerInfo();
|
String serverInfo = getServerInfo();
|
||||||
|
|
||||||
details.name = getXmlString(serverInfo, "hostname");
|
details.name = getXmlString(serverInfo, "hostname", false);
|
||||||
if (details.name == null || details.name.isEmpty()) {
|
if (details.name == null || details.name.isEmpty()) {
|
||||||
details.name = "UNKNOWN";
|
details.name = "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
|
||||||
details.uuid = getXmlString(serverInfo, "uniqueid");
|
// UUID is mandatory to determine which machine is responding
|
||||||
details.macAddress = getXmlString(serverInfo, "mac");
|
details.uuid = getXmlString(serverInfo, "uniqueid", true);
|
||||||
details.localAddress = getXmlString(serverInfo, "LocalIP");
|
|
||||||
|
|
||||||
// This may be null, but that's okay
|
details.macAddress = getXmlString(serverInfo, "mac", false);
|
||||||
details.remoteAddress = getXmlString(serverInfo, "ExternalIP");
|
details.localAddress = getXmlString(serverInfo, "LocalIP", false);
|
||||||
|
|
||||||
|
// This is missing on on recent GFE versions
|
||||||
|
details.remoteAddress = getXmlString(serverInfo, "ExternalIP", false);
|
||||||
|
|
||||||
// This has some extra logic to always report unpaired if the pinned cert isn't there
|
|
||||||
details.pairState = getPairState(serverInfo);
|
details.pairState = getPairState(serverInfo);
|
||||||
|
details.runningGameId = getCurrentGame(serverInfo);
|
||||||
try {
|
|
||||||
details.runningGameId = getCurrentGame(serverInfo);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
details.runningGameId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could reach it so it's online
|
// We could reach it so it's online
|
||||||
details.state = ComputerDetails.State.ONLINE;
|
details.state = ComputerDetails.State.ONLINE;
|
||||||
@@ -290,36 +352,43 @@ public class NvHTTP {
|
|||||||
try {
|
try {
|
||||||
SSLContext sc = SSLContext.getInstance("TLS");
|
SSLContext sc = SSLContext.getInstance("TLS");
|
||||||
sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom());
|
sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom());
|
||||||
return client.newBuilder().sslSocketFactory(sc.getSocketFactory(), trustManager).build();
|
|
||||||
|
// TLS 1.2 is not enabled by default prior to Android 5.0, so we'll need a custom
|
||||||
|
// SSLSocketFactory in order to connect to GFE 3.20.4 which requires TLSv1.2 or later.
|
||||||
|
// We don't just always use TLSv12SocketFactory because explicitly specifying TLS versions
|
||||||
|
// prevents later TLS versions from being negotiated even if client and server otherwise
|
||||||
|
// support them.
|
||||||
|
return client.newBuilder().sslSocketFactory(
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ?
|
||||||
|
sc.getSocketFactory() : new TLSv12SocketFactory(sc),
|
||||||
|
trustManager).build();
|
||||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public X509Certificate getCertificateIfTrusted() {
|
private HttpUrl getCompleteUrl(HttpUrl baseUrl, String path, String query) {
|
||||||
try {
|
return baseUrl.newBuilder()
|
||||||
Response resp = httpClient.newCall(new Request.Builder().url(baseUrlHttps).get().build()).execute();
|
.addPathSegment(path)
|
||||||
Handshake handshake = resp.handshake();
|
.query(query)
|
||||||
if (handshake != null) {
|
.addQueryParameter("uniqueid", uniqueId)
|
||||||
return (X509Certificate)handshake.peerCertificates().get(0);
|
.addQueryParameter("uuid", UUID.randomUUID().toString())
|
||||||
}
|
.build();
|
||||||
} catch (IOException ignored) {}
|
}
|
||||||
|
|
||||||
return null;
|
private ResponseBody openHttpConnection(HttpUrl baseUrl, String path, boolean enableReadTimeout) throws IOException {
|
||||||
|
return openHttpConnection(baseUrl, path, null, enableReadTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read timeout should be enabled for any HTTP query that requires no outside action
|
// Read timeout should be enabled for any HTTP query that requires no outside action
|
||||||
// on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit.
|
// on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit.
|
||||||
// The initial pair query does require outside action (user entering a PIN) but subsequent pairing
|
// The initial pair query does require outside action (user entering a PIN) but subsequent pairing
|
||||||
// queries do not.
|
// queries do not.
|
||||||
private ResponseBody openHttpConnection(String url, boolean enableReadTimeout) throws IOException {
|
private ResponseBody openHttpConnection(HttpUrl baseUrl, String path, String query, boolean enableReadTimeout) throws IOException {
|
||||||
Request request = new Request.Builder().url(url).get().build();
|
HttpUrl completeUrl = getCompleteUrl(baseUrl, path, query);
|
||||||
|
Request request = new Request.Builder().url(completeUrl).get().build();
|
||||||
Response response;
|
Response response;
|
||||||
|
|
||||||
if (serverCert == null && !url.startsWith(baseUrlHttp)) {
|
|
||||||
throw new IllegalStateException("Attempted HTTPS fetch without pinned cert");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableReadTimeout) {
|
if (enableReadTimeout) {
|
||||||
response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute();
|
response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute();
|
||||||
}
|
}
|
||||||
@@ -339,25 +408,29 @@ public class NvHTTP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.code() == 404) {
|
if (response.code() == 404) {
|
||||||
throw new FileNotFoundException(url);
|
throw new FileNotFoundException(completeUrl.toString());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new GfeHttpResponseException(response.code(), response.message());
|
throw new GfeHttpResponseException(response.code(), response.message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String openHttpConnectionToString(String url, boolean enableReadTimeout) throws IOException {
|
private String openHttpConnectionToString(HttpUrl baseUrl, String path, boolean enableReadTimeout) throws IOException {
|
||||||
|
return openHttpConnectionToString(baseUrl, path, null, enableReadTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String openHttpConnectionToString(HttpUrl baseUrl, String path, String query, boolean enableReadTimeout) throws IOException {
|
||||||
try {
|
try {
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
LimeLog.info("Requesting URL: "+url);
|
LimeLog.info("Requesting URL: "+getCompleteUrl(baseUrl, path, query));
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseBody resp = openHttpConnection(url, enableReadTimeout);
|
ResponseBody resp = openHttpConnection(baseUrl, path, query, enableReadTimeout);
|
||||||
String respString = resp.string();
|
String respString = resp.string();
|
||||||
resp.close();
|
resp.close();
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
LimeLog.info(url+" -> "+respString);
|
LimeLog.info(getCompleteUrl(baseUrl, path, query)+" -> "+respString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return respString;
|
return respString;
|
||||||
@@ -371,7 +444,8 @@ public class NvHTTP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getServerVersion(String serverInfo) throws XmlPullParserException, IOException {
|
public String getServerVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
return getXmlString(serverInfo, "appversion");
|
// appversion is present in all supported GFE versions
|
||||||
|
return getXmlString(serverInfo, "appversion", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PairingManager.PairState getPairState() throws IOException, XmlPullParserException {
|
public PairingManager.PairState getPairState() throws IOException, XmlPullParserException {
|
||||||
@@ -379,39 +453,26 @@ public class NvHTTP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
|
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
|
||||||
// If we don't have a server cert, we can't be paired even if the host thinks we are
|
// appversion is present in all supported GFE versions
|
||||||
if (serverCert == null) {
|
return NvHTTP.getXmlString(serverInfo, "PairStatus", true).equals("1") ?
|
||||||
return PairState.NOT_PAIRED;
|
PairState.PAIRED : PairState.NOT_PAIRED;
|
||||||
}
|
|
||||||
|
|
||||||
if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) {
|
|
||||||
return PairState.NOT_PAIRED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return PairState.PAIRED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException {
|
public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
String str = getXmlString(serverInfo, "MaxLumaPixelsH264");
|
// MaxLumaPixelsH264 wasn't present on old GFE versions
|
||||||
|
String str = getXmlString(serverInfo, "MaxLumaPixelsH264", false);
|
||||||
if (str != null) {
|
if (str != null) {
|
||||||
try {
|
return Long.parseLong(str);
|
||||||
return Long.parseLong(str);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException {
|
public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
// MaxLumaPixelsHEVC wasn't present on old GFE versions
|
||||||
|
String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC", false);
|
||||||
if (str != null) {
|
if (str != null) {
|
||||||
try {
|
return Long.parseLong(str);
|
||||||
return Long.parseLong(str);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -426,29 +487,28 @@ public class NvHTTP {
|
|||||||
// Bit 10: HEVC Main10 4:4:4
|
// Bit 10: HEVC Main10 4:4:4
|
||||||
// Bit 11: ???
|
// Bit 11: ???
|
||||||
public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException {
|
public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
String str = getXmlString(serverInfo, "ServerCodecModeSupport");
|
// ServerCodecModeSupport wasn't present on old GFE versions
|
||||||
|
String str = getXmlString(serverInfo, "ServerCodecModeSupport", false);
|
||||||
if (str != null) {
|
if (str != null) {
|
||||||
try {
|
return Long.parseLong(str);
|
||||||
return Long.parseLong(str);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getGpuType(String serverInfo) throws XmlPullParserException, IOException {
|
public String getGpuType(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
return getXmlString(serverInfo, "gputype");
|
// ServerCodecModeSupport wasn't present on old GFE versions
|
||||||
|
return getXmlString(serverInfo, "gputype", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getGfeVersion(String serverInfo) throws XmlPullParserException, IOException {
|
public String getGfeVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
return getXmlString(serverInfo, "GfeVersion");
|
// ServerCodecModeSupport wasn't present on old GFE versions
|
||||||
|
return getXmlString(serverInfo, "GfeVersion", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean supports4K(String serverInfo) throws XmlPullParserException, IOException {
|
public boolean supports4K(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
// Only allow 4K on GFE 3.x
|
// Only allow 4K on GFE 3.x. GfeVersion wasn't present on very old versions of GFE.
|
||||||
String gfeVersionStr = getXmlString(serverInfo, "GfeVersion");
|
String gfeVersionStr = getXmlString(serverInfo, "GfeVersion", false);
|
||||||
if (gfeVersionStr == null || gfeVersionStr.startsWith("2.")) {
|
if (gfeVersionStr == null || gfeVersionStr.startsWith("2.")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -460,10 +520,8 @@ public class NvHTTP {
|
|||||||
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
|
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
|
||||||
// has the semantics that its name would indicate. To contain the effects of this change as much
|
// has the semantics that its name would indicate. To contain the effects of this change as much
|
||||||
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
||||||
String serverState = getXmlString(serverInfo, "state");
|
if (getXmlString(serverInfo, "state", true).endsWith("_SERVER_BUSY")) {
|
||||||
if (serverState != null && serverState.endsWith("_SERVER_BUSY")) {
|
return Integer.parseInt(getXmlString(serverInfo, "currentgame", true));
|
||||||
String game = getXmlString(serverInfo, "currentgame");
|
|
||||||
return Integer.parseInt(game);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -529,11 +587,11 @@ public class NvHTTP {
|
|||||||
case (XmlPullParser.TEXT):
|
case (XmlPullParser.TEXT):
|
||||||
NvApp app = appList.getLast();
|
NvApp app = appList.getLast();
|
||||||
if (currentTag.peek().equals("AppTitle")) {
|
if (currentTag.peek().equals("AppTitle")) {
|
||||||
app.setAppName(xpp.getText().trim());
|
app.setAppName(xpp.getText());
|
||||||
} else if (currentTag.peek().equals("ID")) {
|
} else if (currentTag.peek().equals("ID")) {
|
||||||
app.setAppId(xpp.getText().trim());
|
app.setAppId(xpp.getText());
|
||||||
} else if (currentTag.peek().equals("IsHdrSupported")) {
|
} else if (currentTag.peek().equals("IsHdrSupported")) {
|
||||||
app.setHdrSupported(xpp.getText().trim().equals("1"));
|
app.setHdrSupported(xpp.getText().equals("1"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -560,8 +618,8 @@ public class NvHTTP {
|
|||||||
return appList;
|
return appList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAppListRaw() throws MalformedURLException, IOException {
|
public String getAppListRaw() throws IOException {
|
||||||
return openHttpConnectionToString(baseUrlHttps + "/applist?"+buildUniqueIdUuidString(), true);
|
return openHttpConnectionToString(baseUrlHttps, "applist", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException {
|
public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException {
|
||||||
@@ -570,54 +628,52 @@ public class NvHTTP {
|
|||||||
return getAppListByReader(new StringReader(getAppListRaw()));
|
return getAppListByReader(new StringReader(getAppListRaw()));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ResponseBody resp = openHttpConnection(baseUrlHttps + "/applist?" + buildUniqueIdUuidString(), true);
|
ResponseBody resp = openHttpConnection(baseUrlHttps, "applist", true);
|
||||||
LinkedList<NvApp> appList = getAppListByReader(new InputStreamReader(resp.byteStream()));
|
LinkedList<NvApp> appList = getAppListByReader(new InputStreamReader(resp.byteStream()));
|
||||||
resp.close();
|
resp.close();
|
||||||
return appList;
|
return appList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String executePairingCommand(String additionalArguments, boolean enableReadTimeout) throws GfeHttpResponseException, IOException {
|
||||||
|
return openHttpConnectionToString(baseUrlHttp, "pair",
|
||||||
|
"devicename=roth&updateState=1&" + additionalArguments,
|
||||||
|
enableReadTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
String executePairingChallenge() throws GfeHttpResponseException, IOException {
|
||||||
|
return openHttpConnectionToString(baseUrlHttps, "pair",
|
||||||
|
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
public void unpair() throws IOException {
|
public void unpair() throws IOException {
|
||||||
openHttpConnectionToString(baseUrlHttp + "/unpair?"+buildUniqueIdUuidString(), true);
|
openHttpConnectionToString(baseUrlHttp, "unpair", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputStream getBoxArt(NvApp app) throws IOException {
|
public InputStream getBoxArt(NvApp app) throws IOException {
|
||||||
ResponseBody resp = openHttpConnection(baseUrlHttps + "/appasset?"+ buildUniqueIdUuidString() +
|
ResponseBody resp = openHttpConnection(baseUrlHttps, "appasset", "appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true);
|
||||||
"&appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true);
|
|
||||||
return resp.byteStream();
|
return resp.byteStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
|
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
int[] appVersionQuad = getServerAppVersionQuad(serverInfo);
|
return getServerAppVersionQuad(serverInfo)[0];
|
||||||
if (appVersionQuad != null) {
|
|
||||||
return appVersionQuad[0];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException {
|
public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
try {
|
String serverVersion = getServerVersion(serverInfo);
|
||||||
String serverVersion = getServerVersion(serverInfo);
|
if (serverVersion == null) {
|
||||||
if (serverVersion == null) {
|
throw new IllegalArgumentException("Missing server version field");
|
||||||
LimeLog.warning("Missing server version field");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String[] serverVersionSplit = serverVersion.split("\\.");
|
|
||||||
if (serverVersionSplit.length != 4) {
|
|
||||||
LimeLog.warning("Malformed server version field");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int[] ret = new int[serverVersionSplit.length];
|
|
||||||
for (int i = 0; i < ret.length; i++) {
|
|
||||||
ret[i] = Integer.parseInt(serverVersionSplit[i]);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
String[] serverVersionSplit = serverVersion.split("\\.");
|
||||||
|
if (serverVersionSplit.length != 4) {
|
||||||
|
throw new IllegalArgumentException("Malformed server version field: "+serverVersion);
|
||||||
|
}
|
||||||
|
int[] ret = new int[serverVersionSplit.length];
|
||||||
|
for (int i = 0; i < ret.length; i++) {
|
||||||
|
ret[i] = Integer.parseInt(serverVersionSplit[i]);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||||
@@ -632,6 +688,12 @@ public class NvHTTP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException {
|
public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException {
|
||||||
|
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||||
|
// so force it to 0 to ensure the correct resolution is set. We
|
||||||
|
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||||
|
// on GFE 3.20.3.
|
||||||
|
int fps = context.streamConfig.getLaunchRefreshRate() > 60 ? 0 : context.streamConfig.getLaunchRefreshRate();
|
||||||
|
|
||||||
// Using an unsupported resolution (not 720p, 1080p, or 4K) causes
|
// Using an unsupported resolution (not 720p, 1080p, or 4K) causes
|
||||||
// GFE to force SOPS to 720p60. This is fine for < 720p resolutions like
|
// GFE to force SOPS to 720p60. This is fine for < 720p resolutions like
|
||||||
// 360p or 480p, but it is not ideal for 1440p and other resolutions.
|
// 360p or 480p, but it is not ideal for 1440p and other resolutions.
|
||||||
@@ -645,20 +707,9 @@ public class NvHTTP {
|
|||||||
enableSops = false;
|
enableSops = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using SOPS with FPS values over 60 causes GFE to fall back
|
String xmlStr = openHttpConnectionToString(baseUrlHttps, "launch",
|
||||||
// to 720p60. On previous GFE versions, we could avoid this by
|
"appid=" + appId +
|
||||||
// forcing the FPS value to 60 when launching the stream, but
|
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + fps +
|
||||||
// now on GFE 3.20.3 that seems to trigger some sort of
|
|
||||||
// frame rate limiter that locks the game to 60 FPS.
|
|
||||||
if (context.streamConfig.getLaunchRefreshRate() > 60) {
|
|
||||||
LimeLog.info("Disabling SOPS due to high frame rate: "+context.streamConfig.getLaunchRefreshRate());
|
|
||||||
enableSops = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String xmlStr = openHttpConnectionToString(baseUrlHttps +
|
|
||||||
"/launch?" + buildUniqueIdUuidString() +
|
|
||||||
"&appid=" + appId +
|
|
||||||
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + context.streamConfig.getLaunchRefreshRate() +
|
|
||||||
"&additionalStates=1&sops=" + (enableSops ? 1 : 0) +
|
"&additionalStates=1&sops=" + (enableSops ? 1 : 0) +
|
||||||
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||||
"&rikeyid="+context.riKeyId +
|
"&rikeyid="+context.riKeyId +
|
||||||
@@ -668,24 +719,35 @@ public class NvHTTP {
|
|||||||
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") +
|
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") +
|
||||||
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""),
|
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""),
|
||||||
false);
|
false);
|
||||||
String gameSession = getXmlString(xmlStr, "gamesession");
|
if (!getXmlString(xmlStr, "gamesession", true).equals("0")) {
|
||||||
return gameSession != null && !gameSession.equals("0");
|
// sessionUrl0 will be missing for older GFE versions
|
||||||
|
context.rtspSessionUrl = getXmlString(xmlStr, "sessionUrl0", false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException {
|
public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException {
|
||||||
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/resume?" + buildUniqueIdUuidString() +
|
String xmlStr = openHttpConnectionToString(baseUrlHttps, "resume",
|
||||||
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
"rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||||
"&rikeyid="+context.riKeyId +
|
"&rikeyid="+context.riKeyId +
|
||||||
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo(),
|
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo(),
|
||||||
false);
|
false);
|
||||||
String resume = getXmlString(xmlStr, "resume");
|
if (!getXmlString(xmlStr, "resume", true).equals("0")) {
|
||||||
return Integer.parseInt(resume) != 0;
|
// sessionUrl0 will be missing for older GFE versions
|
||||||
|
context.rtspSessionUrl = getXmlString(xmlStr, "sessionUrl0", false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean quitApp() throws IOException, XmlPullParserException {
|
public boolean quitApp() throws IOException, XmlPullParserException {
|
||||||
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/cancel?" + buildUniqueIdUuidString(), false);
|
String xmlStr = openHttpConnectionToString(baseUrlHttps, "cancel", false);
|
||||||
String cancel = getXmlString(xmlStr, "cancel");
|
if (getXmlString(xmlStr, "cancel", true).equals("0")) {
|
||||||
if (Integer.parseInt(cancel) == 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,4 +761,62 @@ public class NvHTTP {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Based on example code from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
|
||||||
|
private static class TLSv12SocketFactory extends SSLSocketFactory {
|
||||||
|
private SSLSocketFactory internalSSLSocketFactory;
|
||||||
|
|
||||||
|
public TLSv12SocketFactory(SSLContext context) {
|
||||||
|
internalSSLSocketFactory = context.getSocketFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getDefaultCipherSuites() {
|
||||||
|
return internalSSLSocketFactory.getDefaultCipherSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getSupportedCipherSuites() {
|
||||||
|
return internalSSLSocketFactory.getSupportedCipherSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket() throws IOException {
|
||||||
|
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||||
|
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(String host, int port) throws IOException {
|
||||||
|
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
||||||
|
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||||
|
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||||
|
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Socket enableTLSv12OnSocket(Socket socket) {
|
||||||
|
if (socket instanceof SSLSocket) {
|
||||||
|
// TLS 1.2 is not enabled by default prior to Android 5.0. We must enable it
|
||||||
|
// explicitly to ensure we can communicate with GFE 3.20.4 which blocks TLS 1.0.
|
||||||
|
((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"});
|
||||||
|
}
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.limelight.nvstream.http;
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import org.bouncycastle.crypto.BlockCipher;
|
||||||
import javax.crypto.SecretKey;
|
import org.bouncycastle.crypto.engines.AESLightEngine;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ import java.security.*;
|
|||||||
import java.security.cert.*;
|
import java.security.cert.*;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class PairingManager {
|
public class PairingManager {
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ public class PairingManager {
|
|||||||
|
|
||||||
private PrivateKey pk;
|
private PrivateKey pk;
|
||||||
private X509Certificate cert;
|
private X509Certificate cert;
|
||||||
private SecretKey aesKey;
|
|
||||||
private byte[] pemCertBytes;
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
private X509Certificate serverCert;
|
private X509Certificate serverCert;
|
||||||
@@ -55,6 +53,10 @@ public class PairingManager {
|
|||||||
|
|
||||||
private static byte[] hexToBytes(String s) {
|
private static byte[] hexToBytes(String s) {
|
||||||
int len = s.length();
|
int len = s.length();
|
||||||
|
if (len % 2 != 0) {
|
||||||
|
throw new IllegalArgumentException("Illegal string length: "+len);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] data = new byte[len / 2];
|
byte[] data = new byte[len / 2];
|
||||||
for (int i = 0; i < len; i += 2) {
|
for (int i = 0; i < len; i += 2) {
|
||||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||||
@@ -65,7 +67,8 @@ public class PairingManager {
|
|||||||
|
|
||||||
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
||||||
{
|
{
|
||||||
String certText = NvHTTP.getXmlString(text, "plaincert");
|
// Plaincert may be null if another client is already trying to pair
|
||||||
|
String certText = NvHTTP.getXmlString(text, "plaincert", false);
|
||||||
if (certText != null) {
|
if (certText != null) {
|
||||||
byte[] certBytes = hexToBytes(certText);
|
byte[] certBytes = hexToBytes(certText);
|
||||||
|
|
||||||
@@ -74,7 +77,7 @@ public class PairingManager {
|
|||||||
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -121,43 +124,35 @@ public class PairingManager {
|
|||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
|
||||||
try {
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
|
||||||
|
|
||||||
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
|
||||||
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
|
||||||
byte[] fullDecrypted = new byte[blockRoundedSize];
|
|
||||||
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
private static byte[] performBlockCipher(BlockCipher blockCipher, byte[] input) {
|
||||||
cipher.doFinal(blockRoundedEncrypted, 0,
|
int blockSize = blockCipher.getBlockSize();
|
||||||
blockRoundedSize, fullDecrypted);
|
int blockRoundedSize = (input.length + (blockSize - 1)) & ~(blockSize - 1);
|
||||||
return fullDecrypted;
|
|
||||||
} catch (GeneralSecurityException e) {
|
byte[] blockRoundedInputData = Arrays.copyOf(input, blockRoundedSize);
|
||||||
e.printStackTrace();
|
byte[] blockRoundedOutputData = new byte[blockRoundedSize];
|
||||||
throw new RuntimeException(e);
|
|
||||||
|
for (int offset = 0; offset < blockRoundedSize; offset += blockSize) {
|
||||||
|
blockCipher.processBlock(blockRoundedInputData, offset, blockRoundedOutputData, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return blockRoundedOutputData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
private static byte[] decryptAes(byte[] encryptedData, byte[] aesKey) {
|
||||||
try {
|
BlockCipher aesEngine = new AESLightEngine();
|
||||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
aesEngine.init(false, new KeyParameter(aesKey));
|
||||||
|
return performBlockCipher(aesEngine, encryptedData);
|
||||||
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
|
||||||
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
|
||||||
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
|
||||||
return cipher.doFinal(blockRoundedData);
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
private static byte[] encryptAes(byte[] plaintextData, byte[] aesKey) {
|
||||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
BlockCipher aesEngine = new AESLightEngine();
|
||||||
return new SecretKeySpec(aesTruncated, "AES");
|
aesEngine.init(true, new KeyParameter(aesKey));
|
||||||
|
return performBlockCipher(aesEngine, plaintextData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||||
|
return Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] concatBytes(byte[] a, byte[] b) {
|
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||||
@@ -168,7 +163,7 @@ public class PairingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String generatePinString() {
|
public static String generatePinString() {
|
||||||
Random r = new Random();
|
SecureRandom r = new SecureRandom();
|
||||||
return String.format((Locale)null, "%d%d%d%d",
|
return String.format((Locale)null, "%d%d%d%d",
|
||||||
r.nextInt(10), r.nextInt(10),
|
r.nextInt(10), r.nextInt(10),
|
||||||
r.nextInt(10), r.nextInt(10));
|
r.nextInt(10), r.nextInt(10));
|
||||||
@@ -196,16 +191,14 @@ public class PairingManager {
|
|||||||
byte[] salt = generateRandomBytes(16);
|
byte[] salt = generateRandomBytes(16);
|
||||||
|
|
||||||
// Combine the salt and pin, then create an AES key from them
|
// Combine the salt and pin, then create an AES key from them
|
||||||
byte[] saltAndPin = saltPin(salt, pin);
|
byte[] aesKey = generateAesKey(hashAlgo, saltPin(salt, pin));
|
||||||
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
|
||||||
|
|
||||||
// Send the salt and get the server cert. This doesn't have a read timeout
|
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||||
// because the user must enter the PIN before the server responds
|
// because the user must enter the PIN before the server responds
|
||||||
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
String getCert = http.executePairingCommand("phrase=getservercert&salt="+
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
|
||||||
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||||
false);
|
false);
|
||||||
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
if (!NvHTTP.getXmlString(getCert, "paired", true).equals("1")) {
|
||||||
return PairState.FAILED;
|
return PairState.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +207,7 @@ public class PairingManager {
|
|||||||
if (serverCert == null) {
|
if (serverCert == null) {
|
||||||
// Attempting to pair while another device is pairing will cause GFE
|
// Attempting to pair while another device is pairing will cause GFE
|
||||||
// to give an empty cert in the response.
|
// to give an empty cert in the response.
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
http.unpair();
|
||||||
return PairState.ALREADY_IN_PROGRESS;
|
return PairState.ALREADY_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,16 +219,14 @@ public class PairingManager {
|
|||||||
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||||
|
|
||||||
// Send the encrypted challenge to the server
|
// Send the encrypted challenge to the server
|
||||||
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
String challengeResp = http.executePairingCommand("clientchallenge="+bytesToHex(encryptedChallenge), true);
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
if (!NvHTTP.getXmlString(challengeResp, "paired", true).equals("1")) {
|
||||||
true);
|
http.unpair();
|
||||||
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
|
||||||
return PairState.FAILED;
|
return PairState.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the server's response and subsequent challenge
|
// Decode the server's response and subsequent challenge
|
||||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse", true));
|
||||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||||
|
|
||||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||||
@@ -245,23 +236,21 @@ public class PairingManager {
|
|||||||
byte[] clientSecret = generateRandomBytes(16);
|
byte[] clientSecret = generateRandomBytes(16);
|
||||||
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
String secretResp = http.executePairingCommand("serverchallengeresp="+bytesToHex(challengeRespEncrypted), true);
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
if (!NvHTTP.getXmlString(secretResp, "paired", true).equals("1")) {
|
||||||
true);
|
http.unpair();
|
||||||
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
|
||||||
return PairState.FAILED;
|
return PairState.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the server's signed secret
|
// Get the server's signed secret
|
||||||
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret", true));
|
||||||
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||||
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||||
|
|
||||||
// Ensure the authenticity of the data
|
// Ensure the authenticity of the data
|
||||||
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||||
// Cancel the pairing process
|
// Cancel the pairing process
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
http.unpair();
|
||||||
|
|
||||||
// Looks like a MITM
|
// Looks like a MITM
|
||||||
return PairState.FAILED;
|
return PairState.FAILED;
|
||||||
@@ -271,7 +260,7 @@ public class PairingManager {
|
|||||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||||
// Cancel the pairing process
|
// Cancel the pairing process
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
http.unpair();
|
||||||
|
|
||||||
// Probably got the wrong PIN
|
// Probably got the wrong PIN
|
||||||
return PairState.PIN_WRONG;
|
return PairState.PIN_WRONG;
|
||||||
@@ -279,19 +268,16 @@ public class PairingManager {
|
|||||||
|
|
||||||
// Send the server our signed secret
|
// Send the server our signed secret
|
||||||
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||||
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
String clientSecretResp = http.executePairingCommand("clientpairingsecret="+bytesToHex(clientPairingSecret), true);
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
if (!NvHTTP.getXmlString(clientSecretResp, "paired", true).equals("1")) {
|
||||||
true);
|
http.unpair();
|
||||||
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
|
||||||
return PairState.FAILED;
|
return PairState.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the initial challenge (seems neccessary for us to show as paired)
|
// Do the initial challenge (seems necessary for us to show as paired)
|
||||||
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
String pairChallenge = http.executePairingChallenge();
|
||||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
if (!NvHTTP.getXmlString(pairChallenge, "paired", true).equals("1")) {
|
||||||
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
http.unpair();
|
||||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
|
||||||
return PairState.FAILED;
|
return PairState.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,9 +300,8 @@ public class PairingManager {
|
|||||||
return md.digest(data);
|
return md.digest(data);
|
||||||
}
|
}
|
||||||
catch (NoSuchAlgorithmException e) {
|
catch (NoSuchAlgorithmException e) {
|
||||||
// Shouldn't ever happen
|
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,9 +317,8 @@ public class PairingManager {
|
|||||||
return md.digest(data);
|
return md.digest(data);
|
||||||
}
|
}
|
||||||
catch (NoSuchAlgorithmException e) {
|
catch (NoSuchAlgorithmException e) {
|
||||||
// Shouldn't ever happen
|
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,18 @@ public class MoonBridge {
|
|||||||
public static final int VIDEO_FORMAT_MASK_H264 = 0x00FF;
|
public static final int VIDEO_FORMAT_MASK_H264 = 0x00FF;
|
||||||
public static final int VIDEO_FORMAT_MASK_H265 = 0xFF00;
|
public static final int VIDEO_FORMAT_MASK_H265 = 0xFF00;
|
||||||
|
|
||||||
|
public static final int ENCFLG_NONE = 0;
|
||||||
|
public static final int ENCFLG_AUDIO = 1;
|
||||||
|
public static final int ENCFLG_ALL = 0xFFFFFFFF;
|
||||||
|
|
||||||
public static final int BUFFER_TYPE_PICDATA = 0;
|
public static final int BUFFER_TYPE_PICDATA = 0;
|
||||||
public static final int BUFFER_TYPE_SPS = 1;
|
public static final int BUFFER_TYPE_SPS = 1;
|
||||||
public static final int BUFFER_TYPE_PPS = 2;
|
public static final int BUFFER_TYPE_PPS = 2;
|
||||||
public static final int BUFFER_TYPE_VPS = 3;
|
public static final int BUFFER_TYPE_VPS = 3;
|
||||||
|
|
||||||
|
public static final int FRAME_TYPE_PFRAME = 0;
|
||||||
|
public static final int FRAME_TYPE_IDR = 1;
|
||||||
|
|
||||||
public static final int CAPABILITY_DIRECT_SUBMIT = 1;
|
public static final int CAPABILITY_DIRECT_SUBMIT = 1;
|
||||||
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC = 2;
|
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC = 2;
|
||||||
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC = 4;
|
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC = 4;
|
||||||
@@ -33,6 +40,31 @@ public class MoonBridge {
|
|||||||
public static final int CONN_STATUS_OKAY = 0;
|
public static final int CONN_STATUS_OKAY = 0;
|
||||||
public static final int CONN_STATUS_POOR = 1;
|
public static final int CONN_STATUS_POOR = 1;
|
||||||
|
|
||||||
|
public static final int ML_ERROR_GRACEFUL_TERMINATION = 0;
|
||||||
|
public static final int ML_ERROR_NO_VIDEO_TRAFFIC = -100;
|
||||||
|
public static final int ML_ERROR_NO_VIDEO_FRAME = -101;
|
||||||
|
public static final int ML_ERROR_UNEXPECTED_EARLY_TERMINATION = -102;
|
||||||
|
public static final int ML_ERROR_PROTECTED_CONTENT = -103;
|
||||||
|
|
||||||
|
public static final int ML_PORT_INDEX_TCP_47984 = 0;
|
||||||
|
public static final int ML_PORT_INDEX_TCP_47989 = 1;
|
||||||
|
public static final int ML_PORT_INDEX_TCP_48010 = 2;
|
||||||
|
public static final int ML_PORT_INDEX_UDP_47998 = 8;
|
||||||
|
public static final int ML_PORT_INDEX_UDP_47999 = 9;
|
||||||
|
public static final int ML_PORT_INDEX_UDP_48000 = 10;
|
||||||
|
public static final int ML_PORT_INDEX_UDP_48010 = 11;
|
||||||
|
|
||||||
|
public static final int ML_PORT_FLAG_ALL = 0xFFFFFFFF;
|
||||||
|
public static final int ML_PORT_FLAG_TCP_47984 = 0x0001;
|
||||||
|
public static final int ML_PORT_FLAG_TCP_47989 = 0x0002;
|
||||||
|
public static final int ML_PORT_FLAG_TCP_48010 = 0x0004;
|
||||||
|
public static final int ML_PORT_FLAG_UDP_47998 = 0x0100;
|
||||||
|
public static final int ML_PORT_FLAG_UDP_47999 = 0x0200;
|
||||||
|
public static final int ML_PORT_FLAG_UDP_48000 = 0x0400;
|
||||||
|
public static final int ML_PORT_FLAG_UDP_48010 = 0x0800;
|
||||||
|
|
||||||
|
public static final int ML_TEST_RESULT_INCONCLUSIVE = 0xFFFFFFFF;
|
||||||
|
|
||||||
private static AudioRenderer audioRenderer;
|
private static AudioRenderer audioRenderer;
|
||||||
private static VideoDecoderRenderer videoRenderer;
|
private static VideoDecoderRenderer videoRenderer;
|
||||||
private static NvConnectionListener connectionListener;
|
private static NvConnectionListener connectionListener;
|
||||||
@@ -124,12 +156,12 @@ public class MoonBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int bridgeDrSubmitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength,
|
public static int bridgeDrSubmitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||||
int decodeUnitType,
|
int frameNumber, int frameType,
|
||||||
int frameNumber, long receiveTimeMs) {
|
long receiveTimeMs, long enqueueTimeMs) {
|
||||||
if (videoRenderer != null) {
|
if (videoRenderer != null) {
|
||||||
return videoRenderer.submitDecodeUnit(decodeUnitData, decodeUnitLength,
|
return videoRenderer.submitDecodeUnit(decodeUnitData, decodeUnitLength,
|
||||||
decodeUnitType, frameNumber, receiveTimeMs);
|
decodeUnitType, frameNumber, frameType, receiveTimeMs, enqueueTimeMs);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return DR_OK;
|
return DR_OK;
|
||||||
@@ -183,7 +215,7 @@ public class MoonBridge {
|
|||||||
|
|
||||||
public static void bridgeClStageFailed(int stage, int errorCode) {
|
public static void bridgeClStageFailed(int stage, int errorCode) {
|
||||||
if (connectionListener != null) {
|
if (connectionListener != null) {
|
||||||
connectionListener.stageFailed(getStageName(stage), errorCode);
|
connectionListener.stageFailed(getStageName(stage), getPortFlagsFromStage(stage), errorCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +243,12 @@ public class MoonBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void bridgeClSetHdrMode(boolean enabled) {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.setHdrMode(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void setupBridge(VideoDecoderRenderer videoRenderer, AudioRenderer audioRenderer, NvConnectionListener connectionListener) {
|
public static void setupBridge(VideoDecoderRenderer videoRenderer, AudioRenderer audioRenderer, NvConnectionListener connectionListener) {
|
||||||
MoonBridge.videoRenderer = videoRenderer;
|
MoonBridge.videoRenderer = videoRenderer;
|
||||||
MoonBridge.audioRenderer = audioRenderer;
|
MoonBridge.audioRenderer = audioRenderer;
|
||||||
@@ -224,12 +262,14 @@ public class MoonBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static native int startConnection(String address, String appVersion, String gfeVersion,
|
public static native int startConnection(String address, String appVersion, String gfeVersion,
|
||||||
|
String rtspSessionUrl,
|
||||||
int width, int height, int fps,
|
int width, int height, int fps,
|
||||||
int bitrate, int packetSize, int streamingRemotely,
|
int bitrate, int packetSize, int streamingRemotely,
|
||||||
int audioConfiguration, boolean supportsHevc,
|
int audioConfiguration, boolean supportsHevc,
|
||||||
boolean enableHdr,
|
boolean enableHdr,
|
||||||
int hevcBitratePercentageMultiplier,
|
int hevcBitratePercentageMultiplier,
|
||||||
int clientRefreshRateX100,
|
int clientRefreshRateX100,
|
||||||
|
int encryptionFlags,
|
||||||
byte[] riAesKey, byte[] riAesIv,
|
byte[] riAesKey, byte[] riAesIv,
|
||||||
int videoCapabilities);
|
int videoCapabilities);
|
||||||
|
|
||||||
@@ -241,6 +281,8 @@ public class MoonBridge {
|
|||||||
|
|
||||||
public static native void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight);
|
public static native void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight);
|
||||||
|
|
||||||
|
public static native void sendMouseMoveAsMousePosition(short deltaX, short deltaY, short referenceWidth, short referenceHeight);
|
||||||
|
|
||||||
public static native void sendMouseButton(byte buttonEvent, byte mouseButton);
|
public static native void sendMouseButton(byte buttonEvent, byte mouseButton);
|
||||||
|
|
||||||
public static native void sendMultiControllerInput(short controllerNumber,
|
public static native void sendMultiControllerInput(short controllerNumber,
|
||||||
@@ -258,6 +300,10 @@ public class MoonBridge {
|
|||||||
|
|
||||||
public static native void sendMouseScroll(byte scrollClicks);
|
public static native void sendMouseScroll(byte scrollClicks);
|
||||||
|
|
||||||
|
public static native void sendMouseHighResScroll(short scrollAmount);
|
||||||
|
|
||||||
|
public static native void sendUtf8Text(String text);
|
||||||
|
|
||||||
public static native String getStageName(int stage);
|
public static native String getStageName(int stage);
|
||||||
|
|
||||||
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
||||||
@@ -266,5 +312,16 @@ public class MoonBridge {
|
|||||||
|
|
||||||
public static native int getPendingVideoFrames();
|
public static native int getPendingVideoFrames();
|
||||||
|
|
||||||
|
public static native int testClientConnectivity(String testServerHostName, int referencePort, int testFlags);
|
||||||
|
|
||||||
|
public static native int getPortFlagsFromStage(int stage);
|
||||||
|
|
||||||
|
public static native int getPortFlagsFromTerminationErrorCode(int errorCode);
|
||||||
|
|
||||||
|
public static native String stringifyPortFlags(int portFlags, String separator);
|
||||||
|
|
||||||
|
// The RTT is in the top 32 bits, and the RTT variance is in the bottom 32 bits
|
||||||
|
public static native long getEstimatedRttInfo();
|
||||||
|
|
||||||
public static native void init();
|
public static native void init();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import com.limelight.nvstream.http.ComputerDetails;
|
|||||||
|
|
||||||
public class WakeOnLanSender {
|
public class WakeOnLanSender {
|
||||||
private static final int[] PORTS_TO_TRY = new int[] {
|
private static final int[] PORTS_TO_TRY = new int[] {
|
||||||
7, 9, // Standard WOL ports
|
9, // Standard WOL port (privileged port)
|
||||||
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
|
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||||
|
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||||
};
|
};
|
||||||
|
|
||||||
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import com.limelight.computers.ComputerManagerService;
|
|||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
@@ -86,17 +88,18 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
// Couldn't find a matching interface
|
// Couldn't find a matching interface
|
||||||
return true;
|
return true;
|
||||||
} catch (SocketException e) {
|
} catch (Exception e) {
|
||||||
|
// Catch all exceptions because some broken Android devices
|
||||||
|
// will throw an NPE from inside getNetworkInterfaces().
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doAddPc(String host) {
|
private void doAddPc(String host) throws InterruptedException {
|
||||||
boolean wrongSiteLocal = false;
|
boolean wrongSiteLocal = false;
|
||||||
boolean success;
|
boolean success;
|
||||||
|
int portTestResult;
|
||||||
|
|
||||||
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);
|
||||||
@@ -104,22 +107,30 @@ public class AddComputerManually extends Activity {
|
|||||||
try {
|
try {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
details.manualAddress = host;
|
details.manualAddress = host;
|
||||||
|
|
||||||
try {
|
|
||||||
NvHTTP http = new NvHTTP(host, managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(this));
|
|
||||||
details.serverCert = http.getCertificateIfTrusted();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
|
|
||||||
success = managerBinder.addComputerBlocking(details);
|
success = managerBinder.addComputerBlocking(details);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Propagate the InterruptedException to the caller for proper handling
|
||||||
|
dialog.dismiss();
|
||||||
|
throw e;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||||
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the SpinnerDialog open while testing connectivity
|
||||||
if (!success){
|
if (!success){
|
||||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||||
}
|
}
|
||||||
|
if (!success && !wrongSiteLocal) {
|
||||||
|
// Run the test before dismissing the spinner because it can take a few seconds.
|
||||||
|
portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER, 443,
|
||||||
|
MoonBridge.ML_PORT_FLAG_TCP_47984 | MoonBridge.ML_PORT_FLAG_TCP_47989);
|
||||||
|
} else {
|
||||||
|
// Don't bother with the test if we succeeded or the IP address was bogus
|
||||||
|
portTestResult = MoonBridge.ML_TEST_RESULT_INCONCLUSIVE;
|
||||||
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
|
||||||
@@ -127,7 +138,14 @@ public class AddComputerManually extends Activity {
|
|||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
||||||
}
|
}
|
||||||
else if (!success) {
|
else if (!success) {
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
|
String dialogText;
|
||||||
|
if (portTestResult != MoonBridge.ML_TEST_RESULT_INCONCLUSIVE && portTestResult != 0) {
|
||||||
|
dialogText = getResources().getString(R.string.nettest_text_blocked);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialogText = getResources().getString(R.string.addpc_fail);
|
||||||
|
}
|
||||||
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), dialogText, false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
@@ -150,15 +168,12 @@ public class AddComputerManually extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
while (!isInterrupted()) {
|
while (!isInterrupted()) {
|
||||||
String computer;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
computer = computersToAdd.take();
|
String computer = computersToAdd.take();
|
||||||
|
doAddPc(computer);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
doAddPc(computer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -172,7 +187,14 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
addThread.join();
|
addThread.join();
|
||||||
} catch (InterruptedException ignored) {}
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// InterruptedException clears the thread's interrupt status. Since we can't
|
||||||
|
// handle that here, we will re-interrupt the thread to set the interrupt
|
||||||
|
// status back to true.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
addThread = null;
|
addThread = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ public class PreferenceConfiguration {
|
|||||||
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
||||||
private static final String OSC_OPACITY_PREF_STRING = "seekbar_osc_opacity";
|
private static final String OSC_OPACITY_PREF_STRING = "seekbar_osc_opacity";
|
||||||
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
||||||
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
|
||||||
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
||||||
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
||||||
static final String AUDIO_CONFIG_PREF_STRING = "list_audio_config";
|
static final String AUDIO_CONFIG_PREF_STRING = "list_audio_config";
|
||||||
@@ -31,7 +30,7 @@ public class PreferenceConfiguration {
|
|||||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
||||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
||||||
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
||||||
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
private static final String LEGACY_DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
||||||
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
||||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
||||||
private static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay";
|
private static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay";
|
||||||
@@ -41,23 +40,26 @@ public class PreferenceConfiguration {
|
|||||||
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
||||||
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
||||||
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
||||||
|
private static final String FLIP_FACE_BUTTONS_PREF_STRING = "checkbox_flip_face_buttons";
|
||||||
|
private static final String TOUCHSCREEN_TRACKPAD_PREF_STRING = "checkbox_touchscreen_trackpad";
|
||||||
|
private static final String LATENCY_TOAST_PREF_STRING = "checkbox_enable_post_stream_toast";
|
||||||
|
private static final String FRAME_PACING_PREF_STRING = "frame_pacing";
|
||||||
|
private static final String ABSOLUTE_MOUSE_MODE_PREF_STRING = "checkbox_absolute_mouse_mode";
|
||||||
|
|
||||||
static final String DEFAULT_RESOLUTION = "720p";
|
static final String DEFAULT_RESOLUTION = "1280x720";
|
||||||
static final String DEFAULT_FPS = "60";
|
static final String DEFAULT_FPS = "60";
|
||||||
private static final boolean DEFAULT_STRETCH = false;
|
private static final boolean DEFAULT_STRETCH = false;
|
||||||
private static final boolean DEFAULT_SOPS = true;
|
private static final boolean DEFAULT_SOPS = true;
|
||||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||||
private static final boolean DEFAULT_HOST_AUDIO = false;
|
private static final boolean DEFAULT_HOST_AUDIO = false;
|
||||||
private static final int DEFAULT_DEADZONE = 15;
|
private static final int DEFAULT_DEADZONE = 7;
|
||||||
private static final int DEFAULT_OPACITY = 90;
|
private static final int DEFAULT_OPACITY = 90;
|
||||||
public static final String DEFAULT_LANGUAGE = "default";
|
public static final String DEFAULT_LANGUAGE = "default";
|
||||||
private static final boolean DEFAULT_LIST_MODE = false;
|
|
||||||
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
||||||
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 ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||||
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
||||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
|
||||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
private static final boolean DEFAULT_ENABLE_PIP = false;
|
||||||
private static final boolean DEFAULT_ENABLE_PERF_OVERLAY = false;
|
private static final boolean DEFAULT_ENABLE_PERF_OVERLAY = false;
|
||||||
@@ -67,12 +69,30 @@ public class PreferenceConfiguration {
|
|||||||
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
||||||
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
||||||
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
||||||
|
private static final boolean DEFAULT_FLIP_FACE_BUTTONS = false;
|
||||||
|
private static final boolean DEFAULT_TOUCHSCREEN_TRACKPAD = true;
|
||||||
private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo
|
private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo
|
||||||
|
private static final boolean DEFAULT_LATENCY_TOAST = false;
|
||||||
|
private static final String DEFAULT_FRAME_PACING = "latency";
|
||||||
|
private static final boolean DEFAULT_ABSOLUTE_MOUSE_MODE = 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;
|
||||||
public static final int FORCE_H265_OFF = 1;
|
public static final int FORCE_H265_OFF = 1;
|
||||||
|
|
||||||
|
public static final int FRAME_PACING_MIN_LATENCY = 0;
|
||||||
|
public static final int FRAME_PACING_BALANCED = 1;
|
||||||
|
public static final int FRAME_PACING_CAP_FPS = 2;
|
||||||
|
public static final int FRAME_PACING_MAX_SMOOTHNESS = 3;
|
||||||
|
|
||||||
|
public static final String RES_360P = "640x360";
|
||||||
|
public static final String RES_480P = "854x480";
|
||||||
|
public static final String RES_720P = "1280x720";
|
||||||
|
public static final String RES_1080P = "1920x1080";
|
||||||
|
public static final String RES_1440P = "2560x1440";
|
||||||
|
public static final String RES_4K = "3840x2160";
|
||||||
|
public static final String RES_NATIVE = "Native";
|
||||||
|
|
||||||
public int width, height, fps;
|
public int width, height, fps;
|
||||||
public int bitrate;
|
public int bitrate;
|
||||||
public int videoFormat;
|
public int videoFormat;
|
||||||
@@ -80,73 +100,96 @@ public class PreferenceConfiguration {
|
|||||||
public int oscOpacity;
|
public int oscOpacity;
|
||||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController, usbDriver;
|
public boolean smallIconMode, multiController, usbDriver, flipFaceButtons;
|
||||||
public boolean onscreenController;
|
public boolean onscreenController;
|
||||||
public boolean onlyL3R3;
|
public boolean onlyL3R3;
|
||||||
public boolean disableFrameDrop;
|
|
||||||
public boolean enableHdr;
|
public boolean enableHdr;
|
||||||
public boolean enablePip;
|
public boolean enablePip;
|
||||||
public boolean enablePerfOverlay;
|
public boolean enablePerfOverlay;
|
||||||
|
public boolean enableLatencyToast;
|
||||||
public boolean bindAllUsb;
|
public boolean bindAllUsb;
|
||||||
public boolean mouseEmulation;
|
public boolean mouseEmulation;
|
||||||
public boolean mouseNavButtons;
|
public boolean mouseNavButtons;
|
||||||
public boolean unlockFps;
|
public boolean unlockFps;
|
||||||
public boolean vibrateOsc;
|
public boolean vibrateOsc;
|
||||||
public boolean vibrateFallbackToDevice;
|
public boolean vibrateFallbackToDevice;
|
||||||
|
public boolean touchscreenTrackpad;
|
||||||
public MoonBridge.AudioConfiguration audioConfiguration;
|
public MoonBridge.AudioConfiguration audioConfiguration;
|
||||||
|
public int framePacing;
|
||||||
|
public boolean absoluteMouseMode;
|
||||||
|
|
||||||
private static int getHeightFromResolutionString(String resString) {
|
public static boolean isNativeResolution(int width, int height) {
|
||||||
|
// It's not a native resolution if it matches an existing resolution option
|
||||||
|
if (width == 640 && height == 360) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (width == 854 && height == 480) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (width == 1280 && height == 720) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (width == 1920 && height == 1080) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (width == 2560 && height == 1440) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (width == 3840 && height == 2160) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String convertFromLegacyResolutionString(String resString) {
|
||||||
if (resString.equalsIgnoreCase("360p")) {
|
if (resString.equalsIgnoreCase("360p")) {
|
||||||
return 360;
|
return RES_360P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("480p")) {
|
else if (resString.equalsIgnoreCase("480p")) {
|
||||||
return 480;
|
return RES_480P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("720p")) {
|
else if (resString.equalsIgnoreCase("720p")) {
|
||||||
return 720;
|
return RES_720P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("1080p")) {
|
else if (resString.equalsIgnoreCase("1080p")) {
|
||||||
return 1080;
|
return RES_1080P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("1440p")) {
|
else if (resString.equalsIgnoreCase("1440p")) {
|
||||||
return 1440;
|
return RES_1440P;
|
||||||
}
|
}
|
||||||
else if (resString.equalsIgnoreCase("4K")) {
|
else if (resString.equalsIgnoreCase("4K")) {
|
||||||
return 2160;
|
return RES_4K;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should be unreachable
|
// Should be unreachable
|
||||||
return 720;
|
return RES_720P;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getWidthFromResolutionString(String resString) {
|
private static int getWidthFromResolutionString(String resString) {
|
||||||
int height = getHeightFromResolutionString(resString);
|
return Integer.parseInt(resString.split("x")[0]);
|
||||||
if (height == 480) {
|
}
|
||||||
// This isn't an exact 16:9 resolution
|
|
||||||
return 854;
|
private static int getHeightFromResolutionString(String resString) {
|
||||||
}
|
return Integer.parseInt(resString.split("x")[1]);
|
||||||
else {
|
|
||||||
return (height * 16) / 9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getResolutionString(int width, int height) {
|
private static String getResolutionString(int width, int height) {
|
||||||
switch (height) {
|
switch (height) {
|
||||||
case 360:
|
case 360:
|
||||||
return "360p";
|
return RES_360P;
|
||||||
case 480:
|
case 480:
|
||||||
return "480p";
|
return RES_480P;
|
||||||
default:
|
default:
|
||||||
case 720:
|
case 720:
|
||||||
return "720p";
|
return RES_720P;
|
||||||
case 1080:
|
case 1080:
|
||||||
return "1080p";
|
return RES_1080P;
|
||||||
case 1440:
|
case 1440:
|
||||||
return "1440p";
|
return RES_1440P;
|
||||||
case 2160:
|
case 2160:
|
||||||
return "4K";
|
return RES_4K;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +273,37 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int getFramePacingValue(Context context) {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
// Migrate legacy never drop frames option to the new location
|
||||||
|
if (prefs.contains(LEGACY_DISABLE_FRAME_DROP_PREF_STRING)) {
|
||||||
|
boolean legacyNeverDropFrames = prefs.getBoolean(LEGACY_DISABLE_FRAME_DROP_PREF_STRING, false);
|
||||||
|
prefs.edit()
|
||||||
|
.remove(LEGACY_DISABLE_FRAME_DROP_PREF_STRING)
|
||||||
|
.putString(FRAME_PACING_PREF_STRING, legacyNeverDropFrames ? "balanced" : "latency")
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
String str = prefs.getString(FRAME_PACING_PREF_STRING, DEFAULT_FRAME_PACING);
|
||||||
|
if (str.equals("latency")) {
|
||||||
|
return FRAME_PACING_MIN_LATENCY;
|
||||||
|
}
|
||||||
|
else if (str.equals("balanced")) {
|
||||||
|
return FRAME_PACING_BALANCED;
|
||||||
|
}
|
||||||
|
else if (str.equals("cap-fps")) {
|
||||||
|
return FRAME_PACING_CAP_FPS;
|
||||||
|
}
|
||||||
|
else if (str.equals("smoothness")) {
|
||||||
|
return FRAME_PACING_MAX_SMOOTHNESS;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Should never get here
|
||||||
|
return FRAME_PACING_MIN_LATENCY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void resetStreamingSettings(Context context) {
|
public static void resetStreamingSettings(Context context) {
|
||||||
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
|
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
@@ -317,11 +391,24 @@ public class PreferenceConfiguration {
|
|||||||
else {
|
else {
|
||||||
// Use the new preference location
|
// Use the new preference location
|
||||||
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||||
|
|
||||||
|
// Convert legacy resolution strings to the new style
|
||||||
|
if (!resStr.contains("x")) {
|
||||||
|
resStr = PreferenceConfiguration.convertFromLegacyResolutionString(resStr);
|
||||||
|
prefs.edit().putString(RESOLUTION_PREF_STRING, resStr).apply();
|
||||||
|
}
|
||||||
|
|
||||||
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||||
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
||||||
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!prefs.contains(SMALL_ICONS_PREF_STRING)) {
|
||||||
|
// We need to write small icon mode's default to disk for the settings page to display
|
||||||
|
// the current state of the option properly
|
||||||
|
prefs.edit().putBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)).apply();
|
||||||
|
}
|
||||||
|
|
||||||
// This must happen after the preferences migration to ensure the preferences are populated
|
// This must happen after the preferences migration to ensure the preferences are populated
|
||||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
||||||
if (config.bitrate == 0) {
|
if (config.bitrate == 0) {
|
||||||
@@ -340,6 +427,7 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.videoFormat = getVideoFormatValue(context);
|
config.videoFormat = getVideoFormatValue(context);
|
||||||
|
config.framePacing = getFramePacingValue(context);
|
||||||
|
|
||||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||||
|
|
||||||
@@ -352,13 +440,11 @@ public class PreferenceConfiguration {
|
|||||||
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
||||||
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
||||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||||
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
|
||||||
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
||||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
||||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
||||||
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
||||||
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
|
||||||
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
||||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
||||||
config.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY);
|
config.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY);
|
||||||
@@ -368,6 +454,10 @@ public class PreferenceConfiguration {
|
|||||||
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
||||||
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
||||||
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
||||||
|
config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PREF_STRING, DEFAULT_FLIP_FACE_BUTTONS);
|
||||||
|
config.touchscreenTrackpad = prefs.getBoolean(TOUCHSCREEN_TRACKPAD_PREF_STRING, DEFAULT_TOUCHSCREEN_TRACKPAD);
|
||||||
|
config.enableLatencyToast = prefs.getBoolean(LATENCY_TOAST_PREF_STRING, DEFAULT_LATENCY_TOAST);
|
||||||
|
config.absoluteMouseMode = prefs.getBoolean(ABSOLUTE_MOUSE_MODE_PREF_STRING, DEFAULT_ABSOLUTE_MOUSE_MODE);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import android.widget.LinearLayout;
|
|||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
||||||
public class SeekBarPreference extends DialogPreference
|
public class SeekBarPreference extends DialogPreference
|
||||||
{
|
{
|
||||||
@@ -30,6 +32,8 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
private final int maxValue;
|
private final int maxValue;
|
||||||
private final int minValue;
|
private final int minValue;
|
||||||
private final int stepSize;
|
private final int stepSize;
|
||||||
|
private final int keyStepSize;
|
||||||
|
private final int divisor;
|
||||||
private int currentValue;
|
private int currentValue;
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
@@ -59,6 +63,8 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
||||||
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
||||||
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
||||||
|
divisor = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "divisor", 1);
|
||||||
|
keyStepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "keyStep", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -101,7 +107,14 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String t = String.valueOf(value);
|
String t;
|
||||||
|
if (divisor != 1) {
|
||||||
|
float floatValue = roundedValue / (float)divisor;
|
||||||
|
t = String.format((Locale)null, "%.1f", floatValue);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
t = String.valueOf(value);
|
||||||
|
}
|
||||||
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +132,9 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
}
|
}
|
||||||
|
|
||||||
seekBar.setMax(maxValue);
|
seekBar.setMax(maxValue);
|
||||||
|
if (keyStepSize != 0) {
|
||||||
|
seekBar.setKeyProgressIncrement(keyStepSize);
|
||||||
|
}
|
||||||
seekBar.setProgress(currentValue);
|
seekBar.setProgress(currentValue);
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
@@ -128,6 +144,9 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
protected void onBindDialogView(View v) {
|
protected void onBindDialogView(View v) {
|
||||||
super.onBindDialogView(v);
|
super.onBindDialogView(v);
|
||||||
seekBar.setMax(maxValue);
|
seekBar.setMax(maxValue);
|
||||||
|
if (keyStepSize != 0) {
|
||||||
|
seekBar.setKeyProgressIncrement(keyStepSize);
|
||||||
|
}
|
||||||
seekBar.setProgress(currentValue);
|
seekBar.setProgress(currentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,50 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Vibrator;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceCategory;
|
import android.preference.PreferenceCategory;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Range;
|
import android.util.Range;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
|
import android.view.DisplayCutout;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
|
// HACK for Android 9
|
||||||
|
static DisplayCutout displayCutoutP;
|
||||||
|
|
||||||
void reloadSettings() {
|
void reloadSettings() {
|
||||||
getFragmentManager().beginTransaction().replace(
|
getFragmentManager().beginTransaction().replace(
|
||||||
R.id.stream_settings, new SettingsFragment()
|
R.id.stream_settings, new SettingsFragment()
|
||||||
).commit();
|
).commitAllowingStateLoss();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -43,20 +56,35 @@ public class StreamSettings extends Activity {
|
|||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
|
|
||||||
setContentView(R.layout.activity_stream_settings);
|
setContentView(R.layout.activity_stream_settings);
|
||||||
reloadSettings();
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow();
|
||||||
|
|
||||||
|
// We have to use this hack on Android 9 because we don't have Display.getCutout()
|
||||||
|
// which was added in Android 10.
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
|
||||||
|
// Insets can be null when the activity is recreated on screen rotation
|
||||||
|
// https://stackoverflow.com/questions/61241255/windowinsets-getdisplaycutout-is-null-everywhere-except-within-onattachedtowindo
|
||||||
|
WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
|
||||||
|
if (insets != null) {
|
||||||
|
displayCutoutP = insets.getDisplayCutout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
finish();
|
finish();
|
||||||
|
|
||||||
// Check for changes that require a UI reload to take effect
|
// Check for changes that require a UI reload to take effect
|
||||||
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
|
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
|
||||||
if (newPrefs.listMode != previousPrefs.listMode ||
|
if (!newPrefs.language.equals(previousPrefs.language)) {
|
||||||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
|
|
||||||
!newPrefs.language.equals(previousPrefs.language)) {
|
|
||||||
// Restart the PC view to apply UI changes
|
// Restart the PC view to apply UI changes
|
||||||
Intent intent = new Intent(this, PcView.class);
|
Intent intent = new Intent(this, PcView.class);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
@@ -65,6 +93,7 @@ public class StreamSettings extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
|
private int nativeResolutionStartIndex = Integer.MAX_VALUE;
|
||||||
|
|
||||||
private void setValue(String preferenceKey, String value) {
|
private void setValue(String preferenceKey, String value) {
|
||||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||||
@@ -72,6 +101,47 @@ public class StreamSettings extends Activity {
|
|||||||
pref.setValue(value);
|
pref.setValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addNativeResolutionEntry(int nativeWidth, int nativeHeight, boolean insetsRemoved) {
|
||||||
|
ListPreference pref = (ListPreference) findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING);
|
||||||
|
|
||||||
|
String newName;
|
||||||
|
|
||||||
|
if (insetsRemoved) {
|
||||||
|
newName = getResources().getString(R.string.resolution_prefix_native_fullscreen);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newName = getResources().getString(R.string.resolution_prefix_native);
|
||||||
|
}
|
||||||
|
|
||||||
|
newName += " ("+nativeWidth+"x"+nativeHeight+")";
|
||||||
|
|
||||||
|
String newValue = nativeWidth+"x"+nativeHeight;
|
||||||
|
|
||||||
|
CharSequence[] values = pref.getEntryValues();
|
||||||
|
|
||||||
|
// Check if the native resolution is already present
|
||||||
|
for (CharSequence value : values) {
|
||||||
|
if (newValue.equals(value.toString())) {
|
||||||
|
// It is present in the default list, so don't add it again
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence[] newEntries = Arrays.copyOf(pref.getEntries(), pref.getEntries().length + 1);
|
||||||
|
CharSequence[] newValues = Arrays.copyOf(values, values.length + 1);
|
||||||
|
|
||||||
|
// Add the new native option
|
||||||
|
newEntries[newEntries.length - 1] = newName;
|
||||||
|
newValues[newValues.length - 1] = newValue;
|
||||||
|
|
||||||
|
pref.setEntries(newEntries);
|
||||||
|
pref.setEntryValues(newValues);
|
||||||
|
|
||||||
|
if (newValues.length - 1 < nativeResolutionStartIndex) {
|
||||||
|
nativeResolutionStartIndex = newValues.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||||
int matchingCount = 0;
|
int matchingCount = 0;
|
||||||
|
|
||||||
@@ -108,8 +178,6 @@ public class StreamSettings extends Activity {
|
|||||||
pref.setEntryValues(entryValues);
|
pref.setEntryValues(entryValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||||
@@ -140,19 +208,59 @@ public class StreamSettings extends Activity {
|
|||||||
|
|
||||||
// hide on-screen controls category on non touch screen devices
|
// hide on-screen controls category on non touch screen devices
|
||||||
if (!getActivity().getPackageManager().
|
if (!getActivity().getPackageManager().
|
||||||
hasSystemFeature("android.hardware.touchscreen")) {
|
hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
|
||||||
PreferenceCategory category =
|
{
|
||||||
(PreferenceCategory) findPreference("category_onscreen_controls");
|
PreferenceCategory category =
|
||||||
screen.removePreference(category);
|
(PreferenceCategory) findPreference("category_onscreen_controls");
|
||||||
|
screen.removePreference(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_input_settings");
|
||||||
|
category.removePreference(findPreference("checkbox_touchscreen_trackpad"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove PiP mode on devices pre-Oreo
|
// Hide remote desktop mouse mode on pre-Oreo (which doesn't have pointer capture)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
// and NVIDIA SHIELD devices (which support raw mouse input in pointer capture mode)
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
|
||||||
|
getActivity().getPackageManager().hasSystemFeature("com.nvidia.feature.shield")) {
|
||||||
PreferenceCategory category =
|
PreferenceCategory category =
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
(PreferenceCategory) findPreference("category_input_settings");
|
||||||
|
category.removePreference(findPreference("checkbox_absolute_mouse_mode"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove PiP mode on devices pre-Oreo, where the feature is not available (some low RAM devices),
|
||||||
|
// and on Fire OS where it violates the Amazon App Store guidelines for some reason.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
|
||||||
|
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture") ||
|
||||||
|
getActivity().getPackageManager().hasSystemFeature("com.amazon.software.fireos")) {
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_ui_settings");
|
||||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
category.removePreference(findPreference("checkbox_enable_pip"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire TV apps are not allowed to use WebViews or browsers, so hide the Help category
|
||||||
|
/*if (getActivity().getPackageManager().hasSystemFeature("amazon.hardware.fire_tv")) {
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_help");
|
||||||
|
screen.removePreference(category);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Remove the vibration options if the device can't vibrate
|
||||||
|
if (!((Vibrator)getActivity().getSystemService(Context.VIBRATOR_SERVICE)).hasVibrator()) {
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_input_settings");
|
||||||
|
category.removePreference(findPreference("checkbox_vibrate_fallback"));
|
||||||
|
|
||||||
|
// The entire OSC category may have already been removed by the touchscreen check above
|
||||||
|
category = (PreferenceCategory) findPreference("category_onscreen_controls");
|
||||||
|
if (category != null) {
|
||||||
|
category.removePreference(findPreference("checkbox_vibrate_osc"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int maxSupportedFps = 0;
|
int maxSupportedFps = 0;
|
||||||
|
|
||||||
// Hide non-supported resolution/FPS combinations
|
// Hide non-supported resolution/FPS combinations
|
||||||
@@ -161,6 +269,38 @@ public class StreamSettings extends Activity {
|
|||||||
|
|
||||||
int maxSupportedResW = 0;
|
int maxSupportedResW = 0;
|
||||||
|
|
||||||
|
// Add a native resolution with any insets included for users that don't want content
|
||||||
|
// behind the notch of their display
|
||||||
|
boolean hasInsets = false;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
DisplayCutout cutout;
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
// Use the much nicer Display.getCutout() API on Android 10+
|
||||||
|
cutout = display.getCutout();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Android 9 only
|
||||||
|
cutout = displayCutoutP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cutout != null) {
|
||||||
|
int widthInsets = cutout.getSafeInsetLeft() + cutout.getSafeInsetRight();
|
||||||
|
int heightInsets = cutout.getSafeInsetBottom() + cutout.getSafeInsetTop();
|
||||||
|
|
||||||
|
if (widthInsets != 0 || heightInsets != 0) {
|
||||||
|
DisplayMetrics metrics = new DisplayMetrics();
|
||||||
|
display.getRealMetrics(metrics);
|
||||||
|
|
||||||
|
int width = Math.max(metrics.widthPixels - widthInsets, metrics.heightPixels - heightInsets);
|
||||||
|
int height = Math.min(metrics.widthPixels - widthInsets, metrics.heightPixels - heightInsets);
|
||||||
|
|
||||||
|
addNativeResolutionEntry(width, height, false);
|
||||||
|
hasInsets = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Always allow resolutions that are smaller or equal to the active
|
// Always allow resolutions that are smaller or equal to the active
|
||||||
// display resolution because decoders can report total non-sense to us.
|
// display resolution because decoders can report total non-sense to us.
|
||||||
// For example, a p201 device reports:
|
// For example, a p201 device reports:
|
||||||
@@ -176,6 +316,13 @@ public class StreamSettings extends Activity {
|
|||||||
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
||||||
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
||||||
|
|
||||||
|
// Some TVs report strange values here, so let's avoid native resolutions on a TV
|
||||||
|
// unless they report greater than 4K resolutions.
|
||||||
|
if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION) ||
|
||||||
|
(width > 3840 || height > 2160)) {
|
||||||
|
addNativeResolutionEntry(width, height, hasInsets);
|
||||||
|
}
|
||||||
|
|
||||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||||
maxSupportedResW = 3840;
|
maxSupportedResW = 3840;
|
||||||
}
|
}
|
||||||
@@ -241,33 +388,33 @@ public class StreamSettings extends Activity {
|
|||||||
if (maxSupportedResW != 0) {
|
if (maxSupportedResW != 0) {
|
||||||
if (maxSupportedResW < 3840) {
|
if (maxSupportedResW < 3840) {
|
||||||
// 4K is unsupported
|
// 4K is unsupported
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_4K, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P);
|
||||||
resetBitrateToDefault(prefs, null, null);
|
resetBitrateToDefault(prefs, null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (maxSupportedResW < 2560) {
|
if (maxSupportedResW < 2560) {
|
||||||
// 1440p is unsupported
|
// 1440p is unsupported
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P);
|
||||||
resetBitrateToDefault(prefs, null, null);
|
resetBitrateToDefault(prefs, null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (maxSupportedResW < 1920) {
|
if (maxSupportedResW < 1920) {
|
||||||
// 1080p is unsupported
|
// 1080p is unsupported
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_720P);
|
||||||
resetBitrateToDefault(prefs, null, null);
|
resetBitrateToDefault(prefs, null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -275,6 +422,30 @@ public class StreamSettings extends Activity {
|
|||||||
// Never remove 720p
|
// Never remove 720p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
// On Android 4.2 and later, we can get the true metrics via the
|
||||||
|
// getRealMetrics() function (unlike the lies that getWidth() and getHeight()
|
||||||
|
// tell to us).
|
||||||
|
DisplayMetrics metrics = new DisplayMetrics();
|
||||||
|
getActivity().getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
|
||||||
|
int width = Math.max(metrics.widthPixels, metrics.heightPixels);
|
||||||
|
int height = Math.min(metrics.widthPixels, metrics.heightPixels);
|
||||||
|
addNativeResolutionEntry(width, height, false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// On Android 4.1, we have to resort to reflection to invoke hidden APIs
|
||||||
|
// to get the real screen dimensions.
|
||||||
|
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||||
|
try {
|
||||||
|
Method getRawHeightFunc = Display.class.getMethod("getRawHeight");
|
||||||
|
Method getRawWidthFunc = Display.class.getMethod("getRawWidth");
|
||||||
|
int width = (Integer) getRawWidthFunc.invoke(display);
|
||||||
|
int height = (Integer) getRawHeightFunc.invoke(display);
|
||||||
|
addNativeResolutionEntry(Math.max(width, height), Math.min(width, height), false);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
||||||
// We give some extra room in case the FPS is rounded down
|
// We give some extra room in case the FPS is rounded down
|
||||||
@@ -319,7 +490,7 @@ public class StreamSettings extends Activity {
|
|||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
||||||
PreferenceCategory category =
|
PreferenceCategory category =
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
(PreferenceCategory) findPreference("category_advanced_settings");
|
||||||
category.removePreference(findPreference("checkbox_unlock_fps"));
|
category.removePreference(findPreference("checkbox_unlock_fps"));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -384,6 +555,25 @@ public class StreamSettings extends Activity {
|
|||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
String valueStr = (String) newValue;
|
String valueStr = (String) newValue;
|
||||||
|
|
||||||
|
// Detect if this value is the native resolution option
|
||||||
|
CharSequence[] values = ((ListPreference)preference).getEntryValues();
|
||||||
|
boolean isNativeRes = true;
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
// Look for a match prior to the start of the native resolution entries
|
||||||
|
if (valueStr.equals(values[i].toString()) && i < nativeResolutionStartIndex) {
|
||||||
|
isNativeRes = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is native resolution, show the warning dialog
|
||||||
|
if (isNativeRes) {
|
||||||
|
Dialog.displayDialog(getActivity(),
|
||||||
|
getResources().getString(R.string.title_native_res_dialog),
|
||||||
|
getResources().getString(R.string.text_native_res_dialog),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
// Write the new bitrate value
|
// Write the new bitrate value
|
||||||
resetBitrateToDefault(prefs, valueStr, null);
|
resetBitrateToDefault(prefs, valueStr, null);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import com.limelight.utils.HelpLauncher;
|
||||||
|
|
||||||
|
public class WebLauncherPreference extends Preference {
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
public WebLauncherPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
initialize(attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebLauncherPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
initialize(attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
public WebLauncherPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
initialize(attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialize(AttributeSet attrs) {
|
||||||
|
if (attrs == null) {
|
||||||
|
throw new IllegalStateException("WebLauncherPreference must have attributes!");
|
||||||
|
}
|
||||||
|
|
||||||
|
url = attrs.getAttributeValue(null, "url");
|
||||||
|
if (url == null) {
|
||||||
|
throw new IllegalStateException("WebLauncherPreference must have 'url' attribute!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
HelpLauncher.launchUrl(getContext(), url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,41 +3,22 @@ package com.limelight.utils;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import com.limelight.HelpActivity;
|
import com.limelight.HelpActivity;
|
||||||
|
|
||||||
public class HelpLauncher {
|
public class HelpLauncher {
|
||||||
|
public static void launchUrl(Context context, String url) {
|
||||||
private static boolean isKnownBrowser(Context context, Intent i) {
|
|
||||||
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
|
|
||||||
if (resolvedActivity == null) {
|
|
||||||
// No browser
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String name = resolvedActivity.activityInfo.name;
|
|
||||||
if (name == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
name = name.toLowerCase();
|
|
||||||
return name.contains("chrome") || name.contains("firefox");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void launchUrl(Context context, String url) {
|
|
||||||
// Try to launch the default browser
|
// Try to launch the default browser
|
||||||
try {
|
try {
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
i.setData(Uri.parse(url));
|
i.setData(Uri.parse(url));
|
||||||
|
|
||||||
// Several Android TV devices will lie and say they do have a browser
|
// Several Android TV devices will lie and say they do have a browser even though the OS
|
||||||
// even though the OS just shows an error dialog if we try to use it. We need to
|
// just shows an error dialog if we try to use it. We used to try to be clever and check
|
||||||
// be a bit more clever on these devices and detect if the browser is a legitimate
|
// the package name of the resolved intent, but it's not worth it anymore with Android 11's
|
||||||
// browser or just a fake error message activity.
|
// package visibility changes. We'll just always use the WebView on Android TV.
|
||||||
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
|
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
isKnownBrowser(context, i)) {
|
|
||||||
context.startActivity(i);
|
context.startActivity(i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import com.limelight.AppView;
|
import com.limelight.AppView;
|
||||||
import com.limelight.Game;
|
import com.limelight.Game;
|
||||||
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.ShortcutTrampoline;
|
import com.limelight.ShortcutTrampoline;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
@@ -14,6 +15,7 @@ import com.limelight.nvstream.http.ComputerDetails;
|
|||||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
@@ -23,7 +25,12 @@ import java.net.UnknownHostException;
|
|||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
|
||||||
public class ServerHelper {
|
public class ServerHelper {
|
||||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
public static final String CONNECTION_TEST_SERVER = "android.conntest.moonlight-stream.org";
|
||||||
|
|
||||||
|
public static String getCurrentAddressFromComputer(ComputerDetails computer) throws IOException {
|
||||||
|
if (computer.activeAddress == null) {
|
||||||
|
throw new IOException("No active address for "+computer.name);
|
||||||
|
}
|
||||||
return computer.activeAddress;
|
return computer.activeAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +56,7 @@ public class ServerHelper {
|
|||||||
public static Intent createStartIntent(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, getCurrentAddressFromComputer(computer));
|
intent.putExtra(Game.EXTRA_HOST, computer.activeAddress);
|
||||||
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_APP_HDR, app.isHdrSupported());
|
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||||
@@ -68,14 +75,45 @@ public class ServerHelper {
|
|||||||
|
|
||||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
if (computer.state == ComputerDetails.State.OFFLINE || computer.activeAddress == null) {
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
|
||||||
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void doNetworkTest(final Activity parent) {
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SpinnerDialog spinnerDialog = SpinnerDialog.displayDialog(parent,
|
||||||
|
parent.getResources().getString(R.string.nettest_title_waiting),
|
||||||
|
parent.getResources().getString(R.string.nettest_text_waiting),
|
||||||
|
false);
|
||||||
|
|
||||||
|
int ret = MoonBridge.testClientConnectivity(CONNECTION_TEST_SERVER, 443, MoonBridge.ML_PORT_FLAG_ALL);
|
||||||
|
spinnerDialog.dismiss();
|
||||||
|
|
||||||
|
String dialogSummary;
|
||||||
|
if (ret == MoonBridge.ML_TEST_RESULT_INCONCLUSIVE) {
|
||||||
|
dialogSummary = parent.getResources().getString(R.string.nettest_text_inconclusive);
|
||||||
|
}
|
||||||
|
else if (ret == 0) {
|
||||||
|
dialogSummary = parent.getResources().getString(R.string.nettest_text_success);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialogSummary = parent.getResources().getString(R.string.nettest_text_failure);
|
||||||
|
dialogSummary += MoonBridge.stringifyPortFlags(ret, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.displayDialog(parent,
|
||||||
|
parent.getResources().getString(R.string.nettest_title_done),
|
||||||
|
dialogSummary,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
public static void doQuit(final Activity parent,
|
||||||
final ComputerDetails computer,
|
final ComputerDetails computer,
|
||||||
final NvApp app,
|
final NvApp app,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class ShortcutHelper {
|
|||||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
private void reapShortcutsForDynamicAdd() {
|
private void reapShortcutsForDynamicAdd() {
|
||||||
List<ShortcutInfo> dynamicShortcuts = sm.getDynamicShortcuts();
|
List<ShortcutInfo> dynamicShortcuts = sm.getDynamicShortcuts();
|
||||||
while (dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) {
|
while (!dynamicShortcuts.isEmpty() && dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) {
|
||||||
ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0);
|
ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0);
|
||||||
for (ShortcutInfo scut : dynamicShortcuts) {
|
for (ShortcutInfo scut : dynamicShortcuts) {
|
||||||
if (maxRankShortcut.getRank() < scut.getRank()) {
|
if (maxRankShortcut.getRank() < scut.getRank()) {
|
||||||
@@ -118,8 +118,16 @@ public class ShortcutHelper {
|
|||||||
// To avoid a random carousel of shortcuts popping in and out based on polling status,
|
// To avoid a random carousel of shortcuts popping in and out based on polling status,
|
||||||
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
||||||
// to interact with this PC.
|
// to interact with this PC.
|
||||||
if (forceAdd || sm.getDynamicShortcuts().size() < sm.getMaxShortcutCountPerActivity()) {
|
|
||||||
|
if (forceAdd) {
|
||||||
|
// This should free an entry for us to add one below
|
||||||
reapShortcutsForDynamicAdd();
|
reapShortcutsForDynamicAdd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We still need to check the maximum shortcut count even after reaping,
|
||||||
|
// because there's a possibility that it could be zero.
|
||||||
|
if (sm.getDynamicShortcuts().size() < sm.getMaxShortcutCountPerActivity()) {
|
||||||
|
// Add a shortcut if there is room
|
||||||
sm.addDynamicShortcuts(Collections.singletonList(sinfo));
|
sm.addDynamicShortcuts(Collections.singletonList(sinfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,18 @@ public class TvChannelHelper {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri channelUri = context.getContentResolver().insert(
|
Uri channelUri;
|
||||||
TvContract.Channels.CONTENT_URI, builder.toContentValues());
|
|
||||||
|
try {
|
||||||
|
channelUri = context.getContentResolver().insert(
|
||||||
|
TvContract.Channels.CONTENT_URI, builder.toContentValues());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This can happen on HarmonyOS devices which report to
|
||||||
|
// support Leanback APIs, yet don't implement this URI
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (channelUri != null) {
|
if (channelUri != null) {
|
||||||
long id = ContentUris.parseId(channelUri);
|
long id = ContentUris.parseId(channelUri);
|
||||||
updateChannelIcon(id);
|
updateChannelIcon(id);
|
||||||
@@ -144,8 +154,15 @@ public class TvChannelHelper {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
|
try {
|
||||||
builder.toContentValues());
|
context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
|
||||||
|
builder.toContentValues());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This can happen on HarmonyOS devices which report to
|
||||||
|
// support Leanback APIs, yet don't implement this URI
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TvContract.requestChannelBrowsable(context, channelId);
|
TvContract.requestChannelBrowsable(context, channelId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,17 @@ LOCAL_MODULE := moonlight-core
|
|||||||
LOCAL_SRC_FILES := moonlight-common-c/src/AudioStream.c \
|
LOCAL_SRC_FILES := moonlight-common-c/src/AudioStream.c \
|
||||||
moonlight-common-c/src/ByteBuffer.c \
|
moonlight-common-c/src/ByteBuffer.c \
|
||||||
moonlight-common-c/src/Connection.c \
|
moonlight-common-c/src/Connection.c \
|
||||||
|
moonlight-common-c/src/ConnectionTester.c \
|
||||||
moonlight-common-c/src/ControlStream.c \
|
moonlight-common-c/src/ControlStream.c \
|
||||||
moonlight-common-c/src/FakeCallbacks.c \
|
moonlight-common-c/src/FakeCallbacks.c \
|
||||||
moonlight-common-c/src/InputStream.c \
|
moonlight-common-c/src/InputStream.c \
|
||||||
moonlight-common-c/src/LinkedBlockingQueue.c \
|
moonlight-common-c/src/LinkedBlockingQueue.c \
|
||||||
moonlight-common-c/src/Misc.c \
|
moonlight-common-c/src/Misc.c \
|
||||||
moonlight-common-c/src/Platform.c \
|
moonlight-common-c/src/Platform.c \
|
||||||
|
moonlight-common-c/src/PlatformCrypto.c \
|
||||||
moonlight-common-c/src/PlatformSockets.c \
|
moonlight-common-c/src/PlatformSockets.c \
|
||||||
moonlight-common-c/src/RtpFecQueue.c \
|
moonlight-common-c/src/RtpAudioQueue.c \
|
||||||
moonlight-common-c/src/RtpReorderQueue.c \
|
moonlight-common-c/src/RtpVideoQueue.c \
|
||||||
moonlight-common-c/src/RtspConnection.c \
|
moonlight-common-c/src/RtspConnection.c \
|
||||||
moonlight-common-c/src/RtspParser.c \
|
moonlight-common-c/src/RtspParser.c \
|
||||||
moonlight-common-c/src/SdpGenerator.c \
|
moonlight-common-c/src/SdpGenerator.c \
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
PATH=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH
|
||||||
|
OUTPUT_DIR=~/openssl
|
||||||
|
|
||||||
|
BASE_ARGS="no-shared no-ssl3 no-stdio no-engine no-hw"
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
./Configure android-arm $BASE_ARGS -D__ANDROID_API__=16
|
||||||
|
make clean
|
||||||
|
make build_libs -j`nproc`
|
||||||
|
cp lib*.a $OUTPUT_DIR/armeabi-v7a/
|
||||||
|
|
||||||
|
./Configure android-arm64 $BASE_ARGS -D__ANDROID_API__=21
|
||||||
|
make clean
|
||||||
|
make build_libs -j`nproc`
|
||||||
|
cp lib*.a $OUTPUT_DIR/arm64-v8a/
|
||||||
|
|
||||||
|
./Configure android-x86 $BASE_ARGS -D__ANDROID_API__=16
|
||||||
|
make clean
|
||||||
|
make build_libs -j`nproc`
|
||||||
|
cp lib*.a $OUTPUT_DIR/x86/
|
||||||
|
|
||||||
|
./Configure android-x86_64 $BASE_ARGS -D__ANDROID_API__=21
|
||||||
|
make clean
|
||||||
|
make build_libs -j`nproc`
|
||||||
|
cp lib*.a $OUTPUT_DIR/x86_64/
|
||||||
|
cp -R include/ $OUTPUT_DIR/include
|
||||||
@@ -32,6 +32,7 @@ static jmethodID BridgeClConnectionStartedMethod;
|
|||||||
static jmethodID BridgeClConnectionTerminatedMethod;
|
static jmethodID BridgeClConnectionTerminatedMethod;
|
||||||
static jmethodID BridgeClRumbleMethod;
|
static jmethodID BridgeClRumbleMethod;
|
||||||
static jmethodID BridgeClConnectionStatusUpdateMethod;
|
static jmethodID BridgeClConnectionStatusUpdateMethod;
|
||||||
|
static jmethodID BridgeClSetHdrModeMethod;
|
||||||
static jbyteArray DecodedFrameBuffer;
|
static jbyteArray DecodedFrameBuffer;
|
||||||
static jshortArray DecodedAudioBuffer;
|
static jshortArray DecodedAudioBuffer;
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
|
|||||||
BridgeDrStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStart", "()V");
|
BridgeDrStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStart", "()V");
|
||||||
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
|
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
|
||||||
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
|
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
|
||||||
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I");
|
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIIJJ)I");
|
||||||
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(III)I");
|
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(III)I");
|
||||||
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
|
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
|
||||||
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
|
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
|
||||||
@@ -92,6 +93,7 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
|
|||||||
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(I)V");
|
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(I)V");
|
||||||
BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V");
|
BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V");
|
||||||
BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V");
|
BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V");
|
||||||
|
BridgeClSetHdrModeMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetHdrMode", "(Z)V");
|
||||||
}
|
}
|
||||||
|
|
||||||
int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) {
|
int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) {
|
||||||
@@ -157,7 +159,8 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
|
|||||||
|
|
||||||
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
||||||
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
|
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
|
||||||
decodeUnit->frameNumber, decodeUnit->receiveTimeMs);
|
decodeUnit->frameNumber, decodeUnit->frameType,
|
||||||
|
(jlong)decodeUnit->receiveTimeMs, (jlong)decodeUnit->enqueueTimeMs);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
// We will crash here
|
// We will crash here
|
||||||
(*JVM)->DetachCurrentThread(JVM);
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
@@ -177,8 +180,8 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
|
|||||||
|
|
||||||
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
||||||
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
|
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
|
||||||
decodeUnit->frameNumber,
|
decodeUnit->frameNumber, decodeUnit->frameType,
|
||||||
decodeUnit->receiveTimeMs);
|
(jlong)decodeUnit->receiveTimeMs, (jlong)decodeUnit->enqueueTimeMs);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
// We will crash here
|
// We will crash here
|
||||||
(*JVM)->DetachCurrentThread(JVM);
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
@@ -325,6 +328,16 @@ void BridgeClConnectionStatusUpdate(int connectionStatus) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BridgeClSetHdrMode(bool enabled) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClSetHdrModeMethod, enabled);
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
// We will crash here
|
||||||
|
(*JVM)->DetachCurrentThread(JVM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BridgeClLogMessage(const char* format, ...) {
|
void BridgeClLogMessage(const char* format, ...) {
|
||||||
va_list va;
|
va_list va;
|
||||||
va_start(va, format);
|
va_start(va, format);
|
||||||
@@ -358,23 +371,27 @@ static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
|
|||||||
.logMessage = BridgeClLogMessage,
|
.logMessage = BridgeClLogMessage,
|
||||||
.rumble = BridgeClRumble,
|
.rumble = BridgeClRumble,
|
||||||
.connectionStatusUpdate = BridgeClConnectionStatusUpdate,
|
.connectionStatusUpdate = BridgeClConnectionStatusUpdate,
|
||||||
|
.setHdrMode = BridgeClSetHdrMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass clazz,
|
Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass clazz,
|
||||||
jstring address, jstring appVersion, jstring gfeVersion,
|
jstring address, jstring appVersion, jstring gfeVersion,
|
||||||
|
jstring rtspSessionUrl,
|
||||||
jint width, jint height, jint fps,
|
jint width, jint height, jint fps,
|
||||||
jint bitrate, jint packetSize, jint streamingRemotely,
|
jint bitrate, jint packetSize, jint streamingRemotely,
|
||||||
jint audioConfiguration, jboolean supportsHevc,
|
jint audioConfiguration, jboolean supportsHevc,
|
||||||
jboolean enableHdr,
|
jboolean enableHdr,
|
||||||
jint hevcBitratePercentageMultiplier,
|
jint hevcBitratePercentageMultiplier,
|
||||||
jint clientRefreshRateX100,
|
jint clientRefreshRateX100,
|
||||||
|
jint encryptionFlags,
|
||||||
jbyteArray riAesKey, jbyteArray riAesIv,
|
jbyteArray riAesKey, jbyteArray riAesIv,
|
||||||
jint videoCapabilities) {
|
jint videoCapabilities) {
|
||||||
SERVER_INFORMATION serverInfo = {
|
SERVER_INFORMATION serverInfo = {
|
||||||
.address = (*env)->GetStringUTFChars(env, address, 0),
|
.address = (*env)->GetStringUTFChars(env, address, 0),
|
||||||
.serverInfoAppVersion = (*env)->GetStringUTFChars(env, appVersion, 0),
|
.serverInfoAppVersion = (*env)->GetStringUTFChars(env, appVersion, 0),
|
||||||
.serverInfoGfeVersion = gfeVersion ? (*env)->GetStringUTFChars(env, gfeVersion, 0) : NULL,
|
.serverInfoGfeVersion = gfeVersion ? (*env)->GetStringUTFChars(env, gfeVersion, 0) : NULL,
|
||||||
|
.rtspSessionUrl = rtspSessionUrl ? (*env)->GetStringUTFChars(env, rtspSessionUrl, 0) : NULL,
|
||||||
};
|
};
|
||||||
STREAM_CONFIGURATION streamConfig = {
|
STREAM_CONFIGURATION streamConfig = {
|
||||||
.width = width,
|
.width = width,
|
||||||
@@ -387,7 +404,8 @@ Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass c
|
|||||||
.supportsHevc = supportsHevc,
|
.supportsHevc = supportsHevc,
|
||||||
.enableHdr = enableHdr,
|
.enableHdr = enableHdr,
|
||||||
.hevcBitratePercentageMultiplier = hevcBitratePercentageMultiplier,
|
.hevcBitratePercentageMultiplier = hevcBitratePercentageMultiplier,
|
||||||
.clientRefreshRateX100 = clientRefreshRateX100
|
.clientRefreshRateX100 = clientRefreshRateX100,
|
||||||
|
.encryptionFlags = encryptionFlags,
|
||||||
};
|
};
|
||||||
|
|
||||||
jbyte* riAesKeyBuf = (*env)->GetByteArrayElements(env, riAesKey, NULL);
|
jbyte* riAesKeyBuf = (*env)->GetByteArrayElements(env, riAesKey, NULL);
|
||||||
@@ -413,6 +431,9 @@ Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass c
|
|||||||
if (gfeVersion != NULL) {
|
if (gfeVersion != NULL) {
|
||||||
(*env)->ReleaseStringUTFChars(env, gfeVersion, serverInfo.serverInfoGfeVersion);
|
(*env)->ReleaseStringUTFChars(env, gfeVersion, serverInfo.serverInfoGfeVersion);
|
||||||
}
|
}
|
||||||
|
if (rtspSessionUrl != NULL) {
|
||||||
|
(*env)->ReleaseStringUTFChars(env, rtspSessionUrl, serverInfo.rtspSessionUrl);
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
Submodule app/src/main/jni/moonlight-core/moonlight-common-c updated: 247b1fe0e3...d247873ade
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 2016-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -8,9 +8,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This file is only used by HP C on VMS, and is included automatically
|
* This file is only used by HP C/C++ on VMS, and is included automatically
|
||||||
* after each header file from this directory
|
* after each header file from this directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The C++ compiler doesn't understand these pragmas, even though it
|
||||||
|
* understands the corresponding command line qualifier.
|
||||||
|
*/
|
||||||
|
#ifndef __cplusplus
|
||||||
/* restore state. Must correspond to the save in __decc_include_prologue.h */
|
/* restore state. Must correspond to the save in __decc_include_prologue.h */
|
||||||
#pragma names restore
|
# pragma names restore
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 2016-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -8,13 +8,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This file is only used by HP C on VMS, and is included automatically
|
* This file is only used by HP C/C++ on VMS, and is included automatically
|
||||||
* after each header file from this directory
|
* after each header file from this directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The C++ compiler doesn't understand these pragmas, even though it
|
||||||
|
* understands the corresponding command line qualifier.
|
||||||
|
*/
|
||||||
|
#ifndef __cplusplus
|
||||||
/* save state */
|
/* save state */
|
||||||
#pragma names save
|
# pragma names save
|
||||||
/* have the compiler shorten symbols larger than 31 chars to 23 chars
|
/* have the compiler shorten symbols larger than 31 chars to 23 chars
|
||||||
* followed by a 8 hex char CRC
|
* followed by a 8 hex char CRC
|
||||||
*/
|
*/
|
||||||
#pragma names as_is,shortened
|
# pragma names as_is,shortened
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
#ifndef HEADER_ASN1ERR_H
|
#ifndef HEADER_ASN1ERR_H
|
||||||
# define HEADER_ASN1ERR_H
|
# define HEADER_ASN1ERR_H
|
||||||
|
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
@@ -49,6 +51,7 @@ int ERR_load_ASN1_strings(void);
|
|||||||
# define ASN1_F_ASN1_ITEM_DUP 191
|
# define ASN1_F_ASN1_ITEM_DUP 191
|
||||||
# define ASN1_F_ASN1_ITEM_EMBED_D2I 120
|
# define ASN1_F_ASN1_ITEM_EMBED_D2I 120
|
||||||
# define ASN1_F_ASN1_ITEM_EMBED_NEW 121
|
# define ASN1_F_ASN1_ITEM_EMBED_NEW 121
|
||||||
|
# define ASN1_F_ASN1_ITEM_EX_I2D 144
|
||||||
# define ASN1_F_ASN1_ITEM_FLAGS_I2D 118
|
# define ASN1_F_ASN1_ITEM_FLAGS_I2D 118
|
||||||
# define ASN1_F_ASN1_ITEM_I2D_BIO 192
|
# define ASN1_F_ASN1_ITEM_I2D_BIO 192
|
||||||
# define ASN1_F_ASN1_ITEM_I2D_FP 193
|
# define ASN1_F_ASN1_ITEM_I2D_FP 193
|
||||||
@@ -141,6 +144,7 @@ int ERR_load_ASN1_strings(void);
|
|||||||
# define ASN1_R_ASN1_SIG_PARSE_ERROR 204
|
# define ASN1_R_ASN1_SIG_PARSE_ERROR 204
|
||||||
# define ASN1_R_AUX_ERROR 100
|
# define ASN1_R_AUX_ERROR 100
|
||||||
# define ASN1_R_BAD_OBJECT_HEADER 102
|
# define ASN1_R_BAD_OBJECT_HEADER 102
|
||||||
|
# define ASN1_R_BAD_TEMPLATE 230
|
||||||
# define ASN1_R_BMPSTRING_IS_WRONG_LENGTH 214
|
# define ASN1_R_BMPSTRING_IS_WRONG_LENGTH 214
|
||||||
# define ASN1_R_BN_LIB 105
|
# define ASN1_R_BN_LIB 105
|
||||||
# define ASN1_R_BOOLEAN_IS_WRONG_LENGTH 106
|
# define ASN1_R_BOOLEAN_IS_WRONG_LENGTH 106
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_ASYNCERR_H
|
#ifndef HEADER_ASYNCERR_H
|
||||||
# define HEADER_ASYNCERR_H
|
# define HEADER_ASYNCERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -20,10 +20,6 @@
|
|||||||
# include <openssl/crypto.h>
|
# include <openssl/crypto.h>
|
||||||
# include <openssl/bioerr.h>
|
# include <openssl/bioerr.h>
|
||||||
|
|
||||||
# ifndef OPENSSL_NO_SCTP
|
|
||||||
# include <openssl/e_os2.h>
|
|
||||||
# endif
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
@@ -173,6 +169,7 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
# define BIO_FLAGS_MEM_RDONLY 0x200
|
# define BIO_FLAGS_MEM_RDONLY 0x200
|
||||||
# define BIO_FLAGS_NONCLEAR_RST 0x400
|
# define BIO_FLAGS_NONCLEAR_RST 0x400
|
||||||
|
# define BIO_FLAGS_IN_EOF 0x800
|
||||||
|
|
||||||
typedef union bio_addr_st BIO_ADDR;
|
typedef union bio_addr_st BIO_ADDR;
|
||||||
typedef struct bio_addrinfo_st BIO_ADDRINFO;
|
typedef struct bio_addrinfo_st BIO_ADDRINFO;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_BIOERR_H
|
#ifndef HEADER_BIOERR_H
|
||||||
# define HEADER_BIOERR_H
|
# define HEADER_BIOERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
* Copyright (c) 2002, Oracle and/or its affiliates. All rights reserved
|
* Copyright (c) 2002, Oracle and/or its affiliates. All rights reserved
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
@@ -56,7 +56,7 @@ extern "C" {
|
|||||||
* avoid leaking exponent information through timing,
|
* avoid leaking exponent information through timing,
|
||||||
* BN_mod_exp_mont() will call BN_mod_exp_mont_consttime,
|
* BN_mod_exp_mont() will call BN_mod_exp_mont_consttime,
|
||||||
* BN_div() will call BN_div_no_branch,
|
* BN_div() will call BN_div_no_branch,
|
||||||
* BN_mod_inverse() will call BN_mod_inverse_no_branch.
|
* BN_mod_inverse() will call bn_mod_inverse_no_branch.
|
||||||
*/
|
*/
|
||||||
# define BN_FLG_CONSTTIME 0x04
|
# define BN_FLG_CONSTTIME 0x04
|
||||||
# define BN_FLG_SECURE 0x08
|
# define BN_FLG_SECURE 0x08
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_BNERR_H
|
#ifndef HEADER_BNERR_H
|
||||||
# define HEADER_BNERR_H
|
# define HEADER_BNERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_BUFERR_H
|
#ifndef HEADER_BUFERR_H
|
||||||
# define HEADER_BUFERR_H
|
# define HEADER_BUFERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2008-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 2008-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -284,8 +284,6 @@ int CMS_unsigned_add1_attr_by_txt(CMS_SignerInfo *si,
|
|||||||
void *CMS_unsigned_get0_data_by_OBJ(CMS_SignerInfo *si, ASN1_OBJECT *oid,
|
void *CMS_unsigned_get0_data_by_OBJ(CMS_SignerInfo *si, ASN1_OBJECT *oid,
|
||||||
int lastpos, int type);
|
int lastpos, int type);
|
||||||
|
|
||||||
# ifdef HEADER_X509V3_H
|
|
||||||
|
|
||||||
int CMS_get1_ReceiptRequest(CMS_SignerInfo *si, CMS_ReceiptRequest **prr);
|
int CMS_get1_ReceiptRequest(CMS_SignerInfo *si, CMS_ReceiptRequest **prr);
|
||||||
CMS_ReceiptRequest *CMS_ReceiptRequest_create0(unsigned char *id, int idlen,
|
CMS_ReceiptRequest *CMS_ReceiptRequest_create0(unsigned char *id, int idlen,
|
||||||
int allorfirst,
|
int allorfirst,
|
||||||
@@ -298,7 +296,6 @@ void CMS_ReceiptRequest_get0_values(CMS_ReceiptRequest *rr,
|
|||||||
int *pallorfirst,
|
int *pallorfirst,
|
||||||
STACK_OF(GENERAL_NAMES) **plist,
|
STACK_OF(GENERAL_NAMES) **plist,
|
||||||
STACK_OF(GENERAL_NAMES) **prto);
|
STACK_OF(GENERAL_NAMES) **prto);
|
||||||
# endif
|
|
||||||
int CMS_RecipientInfo_kari_get0_alg(CMS_RecipientInfo *ri,
|
int CMS_RecipientInfo_kari_get0_alg(CMS_RecipientInfo *ri,
|
||||||
X509_ALGOR **palg,
|
X509_ALGOR **palg,
|
||||||
ASN1_OCTET_STRING **pukm);
|
ASN1_OCTET_STRING **pukm);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_CMSERR_H
|
#ifndef HEADER_CMSERR_H
|
||||||
# define HEADER_CMSERR_H
|
# define HEADER_CMSERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# include <openssl/opensslconf.h>
|
# include <openssl/opensslconf.h>
|
||||||
|
|
||||||
# ifndef OPENSSL_NO_CMS
|
# ifndef OPENSSL_NO_CMS
|
||||||
@@ -101,6 +105,7 @@ int ERR_load_CMS_strings(void);
|
|||||||
# define CMS_F_CMS_SIGNERINFO_VERIFY_CERT 153
|
# define CMS_F_CMS_SIGNERINFO_VERIFY_CERT 153
|
||||||
# define CMS_F_CMS_SIGNERINFO_VERIFY_CONTENT 154
|
# define CMS_F_CMS_SIGNERINFO_VERIFY_CONTENT 154
|
||||||
# define CMS_F_CMS_SIGN_RECEIPT 163
|
# define CMS_F_CMS_SIGN_RECEIPT 163
|
||||||
|
# define CMS_F_CMS_SI_CHECK_ATTRIBUTES 183
|
||||||
# define CMS_F_CMS_STREAM 155
|
# define CMS_F_CMS_STREAM 155
|
||||||
# define CMS_F_CMS_UNCOMPRESS 156
|
# define CMS_F_CMS_UNCOMPRESS 156
|
||||||
# define CMS_F_CMS_VERIFY 157
|
# define CMS_F_CMS_VERIFY 157
|
||||||
@@ -110,6 +115,7 @@ int ERR_load_CMS_strings(void);
|
|||||||
* CMS reason codes.
|
* CMS reason codes.
|
||||||
*/
|
*/
|
||||||
# define CMS_R_ADD_SIGNER_ERROR 99
|
# define CMS_R_ADD_SIGNER_ERROR 99
|
||||||
|
# define CMS_R_ATTRIBUTE_ERROR 161
|
||||||
# define CMS_R_CERTIFICATE_ALREADY_PRESENT 175
|
# define CMS_R_CERTIFICATE_ALREADY_PRESENT 175
|
||||||
# define CMS_R_CERTIFICATE_HAS_NO_KEYID 160
|
# define CMS_R_CERTIFICATE_HAS_NO_KEYID 160
|
||||||
# define CMS_R_CERTIFICATE_VERIFY_ERROR 100
|
# define CMS_R_CERTIFICATE_VERIFY_ERROR 100
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_COMPERR_H
|
#ifndef HEADER_COMPERR_H
|
||||||
# define HEADER_COMPERR_H
|
# define HEADER_COMPERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# include <openssl/opensslconf.h>
|
# include <openssl/opensslconf.h>
|
||||||
|
|
||||||
# ifndef OPENSSL_NO_COMP
|
# ifndef OPENSSL_NO_COMP
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_CONFERR_H
|
#ifndef HEADER_CONFERR_H
|
||||||
# define HEADER_CONFERR_H
|
# define HEADER_CONFERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,12 +11,13 @@
|
|||||||
#ifndef HEADER_CRYPTOERR_H
|
#ifndef HEADER_CRYPTOERR_H
|
||||||
# define HEADER_CRYPTOERR_H
|
# define HEADER_CRYPTOERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
|
|
||||||
# include <openssl/symhacks.h>
|
|
||||||
|
|
||||||
int ERR_load_CRYPTO_strings(void);
|
int ERR_load_CRYPTO_strings(void);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -463,8 +463,6 @@ __owur int CTLOG_STORE_load_file(CTLOG_STORE *store, const char *file);
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Loads the default CT log list into a |store|.
|
* Loads the default CT log list into a |store|.
|
||||||
* See internal/cryptlib.h for the environment variable and file path that are
|
|
||||||
* consulted to find the default file.
|
|
||||||
* Returns 1 if loading is successful, or 0 otherwise.
|
* Returns 1 if loading is successful, or 0 otherwise.
|
||||||
*/
|
*/
|
||||||
__owur int CTLOG_STORE_load_default_file(CTLOG_STORE *store);
|
__owur int CTLOG_STORE_load_default_file(CTLOG_STORE *store);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_CTERR_H
|
#ifndef HEADER_CTERR_H
|
||||||
# define HEADER_CTERR_H
|
# define HEADER_CTERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# include <openssl/opensslconf.h>
|
# include <openssl/opensslconf.h>
|
||||||
|
|
||||||
# ifndef OPENSSL_NO_CT
|
# ifndef OPENSSL_NO_CT
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_DHERR_H
|
#ifndef HEADER_DHERR_H
|
||||||
# define HEADER_DHERR_H
|
# define HEADER_DHERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# include <openssl/opensslconf.h>
|
# include <openssl/opensslconf.h>
|
||||||
|
|
||||||
# ifndef OPENSSL_NO_DH
|
# ifndef OPENSSL_NO_DH
|
||||||
|
|||||||
@@ -162,6 +162,12 @@ DH *DSA_dup_DH(const DSA *r);
|
|||||||
# define EVP_PKEY_CTX_set_dsa_paramgen_bits(ctx, nbits) \
|
# define EVP_PKEY_CTX_set_dsa_paramgen_bits(ctx, nbits) \
|
||||||
EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DSA, EVP_PKEY_OP_PARAMGEN, \
|
EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DSA, EVP_PKEY_OP_PARAMGEN, \
|
||||||
EVP_PKEY_CTRL_DSA_PARAMGEN_BITS, nbits, NULL)
|
EVP_PKEY_CTRL_DSA_PARAMGEN_BITS, nbits, NULL)
|
||||||
|
# define EVP_PKEY_CTX_set_dsa_paramgen_q_bits(ctx, qbits) \
|
||||||
|
EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DSA, EVP_PKEY_OP_PARAMGEN, \
|
||||||
|
EVP_PKEY_CTRL_DSA_PARAMGEN_Q_BITS, qbits, NULL)
|
||||||
|
# define EVP_PKEY_CTX_set_dsa_paramgen_md(ctx, md) \
|
||||||
|
EVP_PKEY_CTX_ctrl(ctx, EVP_PKEY_DSA, EVP_PKEY_OP_PARAMGEN, \
|
||||||
|
EVP_PKEY_CTRL_DSA_PARAMGEN_MD, 0, (void *)(md))
|
||||||
|
|
||||||
# define EVP_PKEY_CTRL_DSA_PARAMGEN_BITS (EVP_PKEY_ALG_CTRL + 1)
|
# define EVP_PKEY_CTRL_DSA_PARAMGEN_BITS (EVP_PKEY_ALG_CTRL + 1)
|
||||||
# define EVP_PKEY_CTRL_DSA_PARAMGEN_Q_BITS (EVP_PKEY_ALG_CTRL + 2)
|
# define EVP_PKEY_CTRL_DSA_PARAMGEN_Q_BITS (EVP_PKEY_ALG_CTRL + 2)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_DSAERR_H
|
#ifndef HEADER_DSAERR_H
|
||||||
# define HEADER_DSAERR_H
|
# define HEADER_DSAERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# include <openssl/opensslconf.h>
|
# include <openssl/opensslconf.h>
|
||||||
|
|
||||||
# ifndef OPENSSL_NO_DSA
|
# ifndef OPENSSL_NO_DSA
|
||||||
@@ -57,6 +61,7 @@ int ERR_load_DSA_strings(void);
|
|||||||
# define DSA_R_INVALID_DIGEST_TYPE 106
|
# define DSA_R_INVALID_DIGEST_TYPE 106
|
||||||
# define DSA_R_INVALID_PARAMETERS 112
|
# define DSA_R_INVALID_PARAMETERS 112
|
||||||
# define DSA_R_MISSING_PARAMETERS 101
|
# define DSA_R_MISSING_PARAMETERS 101
|
||||||
|
# define DSA_R_MISSING_PRIVATE_KEY 111
|
||||||
# define DSA_R_MODULUS_TOO_LARGE 103
|
# define DSA_R_MODULUS_TOO_LARGE 103
|
||||||
# define DSA_R_NO_PARAMETERS_SET 107
|
# define DSA_R_NO_PARAMETERS_SET 107
|
||||||
# define DSA_R_PARAMETER_ENCODING_ERROR 105
|
# define DSA_R_PARAMETER_ENCODING_ERROR 105
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ extern "C" {
|
|||||||
|
|
||||||
# define DTLS1_AL_HEADER_LENGTH 2
|
# define DTLS1_AL_HEADER_LENGTH 2
|
||||||
|
|
||||||
/* Timeout multipliers (timeout slice is defined in apps/timeouts.h */
|
/* Timeout multipliers */
|
||||||
# define DTLS1_TMO_READ_COUNT 2
|
# define DTLS1_TMO_READ_COUNT 2
|
||||||
# define DTLS1_TMO_WRITE_COUNT 2
|
# define DTLS1_TMO_WRITE_COUNT 2
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -241,7 +241,7 @@ typedef UINT64 uint64_t;
|
|||||||
defined(__osf__) || defined(__sgi) || defined(__hpux) || \
|
defined(__osf__) || defined(__sgi) || defined(__hpux) || \
|
||||||
defined(OPENSSL_SYS_VMS) || defined (__OpenBSD__)
|
defined(OPENSSL_SYS_VMS) || defined (__OpenBSD__)
|
||||||
# include <inttypes.h>
|
# include <inttypes.h>
|
||||||
# elif defined(_MSC_VER) && _MSC_VER<=1500
|
# elif defined(_MSC_VER) && _MSC_VER<1600
|
||||||
/*
|
/*
|
||||||
* minimally required typdefs for systems not supporting inttypes.h or
|
* minimally required typdefs for systems not supporting inttypes.h or
|
||||||
* stdint.h: currently just older VC++
|
* stdint.h: currently just older VC++
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 2002-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
* Copyright (c) 2002, Oracle and/or its affiliates. All rights reserved
|
* Copyright (c) 2002, Oracle and/or its affiliates. All rights reserved
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
@@ -142,7 +142,7 @@ const EC_METHOD *EC_GROUP_method_of(const EC_GROUP *group);
|
|||||||
*/
|
*/
|
||||||
int EC_METHOD_get_field_type(const EC_METHOD *meth);
|
int EC_METHOD_get_field_type(const EC_METHOD *meth);
|
||||||
|
|
||||||
/** Sets the generator and it's order/cofactor of a EC_GROUP object.
|
/** Sets the generator and its order/cofactor of a EC_GROUP object.
|
||||||
* \param group EC_GROUP object
|
* \param group EC_GROUP object
|
||||||
* \param generator EC_POINT object with the generator.
|
* \param generator EC_POINT object with the generator.
|
||||||
* \param order the order of the group generated by the generator.
|
* \param order the order of the group generated by the generator.
|
||||||
@@ -829,6 +829,8 @@ void EC_KEY_set_flags(EC_KEY *key, int flags);
|
|||||||
|
|
||||||
void EC_KEY_clear_flags(EC_KEY *key, int flags);
|
void EC_KEY_clear_flags(EC_KEY *key, int flags);
|
||||||
|
|
||||||
|
int EC_KEY_decoded_from_explicit_params(const EC_KEY *key);
|
||||||
|
|
||||||
/** Creates a new EC_KEY object using a named curve as underlying
|
/** Creates a new EC_KEY object using a named curve as underlying
|
||||||
* EC_GROUP object.
|
* EC_GROUP object.
|
||||||
* \param nid NID of the named curve.
|
* \param nid NID of the named curve.
|
||||||
@@ -1138,7 +1140,8 @@ void ECDSA_SIG_free(ECDSA_SIG *sig);
|
|||||||
* (*pp += length of the DER encoded signature)).
|
* (*pp += length of the DER encoded signature)).
|
||||||
* \param sig pointer to the ECDSA_SIG object
|
* \param sig pointer to the ECDSA_SIG object
|
||||||
* \param pp pointer to a unsigned char pointer for the output or NULL
|
* \param pp pointer to a unsigned char pointer for the output or NULL
|
||||||
* \return the length of the DER encoded ECDSA_SIG object or 0
|
* \return the length of the DER encoded ECDSA_SIG object or a negative value
|
||||||
|
* on error
|
||||||
*/
|
*/
|
||||||
int i2d_ECDSA_SIG(const ECDSA_SIG *sig, unsigned char **pp);
|
int i2d_ECDSA_SIG(const ECDSA_SIG *sig, unsigned char **pp);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_ECERR_H
|
#ifndef HEADER_ECERR_H
|
||||||
# define HEADER_ECERR_H
|
# define HEADER_ECERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# include <openssl/opensslconf.h>
|
# include <openssl/opensslconf.h>
|
||||||
|
|
||||||
# ifndef OPENSSL_NO_EC
|
# ifndef OPENSSL_NO_EC
|
||||||
@@ -239,6 +243,7 @@ int ERR_load_EC_strings(void);
|
|||||||
# define EC_R_LADDER_POST_FAILURE 136
|
# define EC_R_LADDER_POST_FAILURE 136
|
||||||
# define EC_R_LADDER_PRE_FAILURE 153
|
# define EC_R_LADDER_PRE_FAILURE 153
|
||||||
# define EC_R_LADDER_STEP_FAILURE 162
|
# define EC_R_LADDER_STEP_FAILURE 162
|
||||||
|
# define EC_R_MISSING_OID 167
|
||||||
# define EC_R_MISSING_PARAMETERS 124
|
# define EC_R_MISSING_PARAMETERS 124
|
||||||
# define EC_R_MISSING_PRIVATE_KEY 125
|
# define EC_R_MISSING_PRIVATE_KEY 125
|
||||||
# define EC_R_NEED_NEW_SETUP_VALUES 157
|
# define EC_R_NEED_NEW_SETUP_VALUES 157
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_ENGINEERR_H
|
#ifndef HEADER_ENGINEERR_H
|
||||||
# define HEADER_ENGINEERR_H
|
# define HEADER_ENGINEERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# include <openssl/opensslconf.h>
|
# include <openssl/opensslconf.h>
|
||||||
|
|
||||||
# ifndef OPENSSL_NO_ENGINE
|
# ifndef OPENSSL_NO_ENGINE
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -37,6 +37,7 @@ extern "C" {
|
|||||||
# define ERR_TXT_STRING 0x02
|
# define ERR_TXT_STRING 0x02
|
||||||
|
|
||||||
# define ERR_FLAG_MARK 0x01
|
# define ERR_FLAG_MARK 0x01
|
||||||
|
# define ERR_FLAG_CLEAR 0x02
|
||||||
|
|
||||||
# define ERR_NUM_ERRORS 16
|
# define ERR_NUM_ERRORS 16
|
||||||
typedef struct err_state_st {
|
typedef struct err_state_st {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ int (*EVP_MD_meth_get_ctrl(const EVP_MD *md))(EVP_MD_CTX *ctx, int cmd,
|
|||||||
* if the following flag is set.
|
* if the following flag is set.
|
||||||
*/
|
*/
|
||||||
# define EVP_MD_CTX_FLAG_FINALISE 0x0200
|
# define EVP_MD_CTX_FLAG_FINALISE 0x0200
|
||||||
/* NOTE: 0x0400 is reserved for internal usage in evp_int.h */
|
/* NOTE: 0x0400 is reserved for internal usage */
|
||||||
|
|
||||||
EVP_CIPHER *EVP_CIPHER_meth_new(int cipher_type, int block_size, int key_len);
|
EVP_CIPHER *EVP_CIPHER_meth_new(int cipher_type, int block_size, int key_len);
|
||||||
EVP_CIPHER *EVP_CIPHER_meth_dup(const EVP_CIPHER *cipher);
|
EVP_CIPHER *EVP_CIPHER_meth_dup(const EVP_CIPHER *cipher);
|
||||||
@@ -260,6 +260,8 @@ int (*EVP_CIPHER_meth_get_ctrl(const EVP_CIPHER *cipher))(EVP_CIPHER_CTX *,
|
|||||||
# define EVP_CIPH_RAND_KEY 0x200
|
# define EVP_CIPH_RAND_KEY 0x200
|
||||||
/* cipher has its own additional copying logic */
|
/* cipher has its own additional copying logic */
|
||||||
# define EVP_CIPH_CUSTOM_COPY 0x400
|
# define EVP_CIPH_CUSTOM_COPY 0x400
|
||||||
|
/* Don't use standard iv length function */
|
||||||
|
# define EVP_CIPH_CUSTOM_IV_LENGTH 0x800
|
||||||
/* Allow use default ASN1 get/set iv */
|
/* Allow use default ASN1 get/set iv */
|
||||||
# define EVP_CIPH_FLAG_DEFAULT_ASN1 0x1000
|
# define EVP_CIPH_FLAG_DEFAULT_ASN1 0x1000
|
||||||
/* Buffer length in bits not bytes: CFB1 mode only */
|
/* Buffer length in bits not bytes: CFB1 mode only */
|
||||||
@@ -349,6 +351,8 @@ int (*EVP_CIPHER_meth_get_ctrl(const EVP_CIPHER *cipher))(EVP_CIPHER_CTX *,
|
|||||||
/* Set the input buffer lengths to use for a pipelined operation */
|
/* Set the input buffer lengths to use for a pipelined operation */
|
||||||
# define EVP_CTRL_SET_PIPELINE_INPUT_LENS 0x24
|
# define EVP_CTRL_SET_PIPELINE_INPUT_LENS 0x24
|
||||||
|
|
||||||
|
# define EVP_CTRL_GET_IVLEN 0x25
|
||||||
|
|
||||||
/* Padding modes */
|
/* Padding modes */
|
||||||
#define EVP_PADDING_PKCS7 1
|
#define EVP_PADDING_PKCS7 1
|
||||||
#define EVP_PADDING_ISO7816_4 2
|
#define EVP_PADDING_ISO7816_4 2
|
||||||
@@ -995,6 +999,7 @@ int EVP_PKEY_set_type_str(EVP_PKEY *pkey, const char *str, int len);
|
|||||||
int EVP_PKEY_set_alias_type(EVP_PKEY *pkey, int type);
|
int EVP_PKEY_set_alias_type(EVP_PKEY *pkey, int type);
|
||||||
# ifndef OPENSSL_NO_ENGINE
|
# ifndef OPENSSL_NO_ENGINE
|
||||||
int EVP_PKEY_set1_engine(EVP_PKEY *pkey, ENGINE *e);
|
int EVP_PKEY_set1_engine(EVP_PKEY *pkey, ENGINE *e);
|
||||||
|
ENGINE *EVP_PKEY_get0_engine(const EVP_PKEY *pkey);
|
||||||
# endif
|
# endif
|
||||||
int EVP_PKEY_assign(EVP_PKEY *pkey, int type, void *key);
|
int EVP_PKEY_assign(EVP_PKEY *pkey, int type, void *key);
|
||||||
void *EVP_PKEY_get0(const EVP_PKEY *pkey);
|
void *EVP_PKEY_get0(const EVP_PKEY *pkey);
|
||||||
@@ -1507,6 +1512,20 @@ void EVP_PKEY_meth_set_ctrl(EVP_PKEY_METHOD *pmeth,
|
|||||||
const char *type,
|
const char *type,
|
||||||
const char *value));
|
const char *value));
|
||||||
|
|
||||||
|
void EVP_PKEY_meth_set_digestsign(EVP_PKEY_METHOD *pmeth,
|
||||||
|
int (*digestsign) (EVP_MD_CTX *ctx,
|
||||||
|
unsigned char *sig,
|
||||||
|
size_t *siglen,
|
||||||
|
const unsigned char *tbs,
|
||||||
|
size_t tbslen));
|
||||||
|
|
||||||
|
void EVP_PKEY_meth_set_digestverify(EVP_PKEY_METHOD *pmeth,
|
||||||
|
int (*digestverify) (EVP_MD_CTX *ctx,
|
||||||
|
const unsigned char *sig,
|
||||||
|
size_t siglen,
|
||||||
|
const unsigned char *tbs,
|
||||||
|
size_t tbslen));
|
||||||
|
|
||||||
void EVP_PKEY_meth_set_check(EVP_PKEY_METHOD *pmeth,
|
void EVP_PKEY_meth_set_check(EVP_PKEY_METHOD *pmeth,
|
||||||
int (*check) (EVP_PKEY *pkey));
|
int (*check) (EVP_PKEY *pkey));
|
||||||
|
|
||||||
@@ -1612,6 +1631,20 @@ void EVP_PKEY_meth_get_ctrl(const EVP_PKEY_METHOD *pmeth,
|
|||||||
const char *type,
|
const char *type,
|
||||||
const char *value));
|
const char *value));
|
||||||
|
|
||||||
|
void EVP_PKEY_meth_get_digestsign(EVP_PKEY_METHOD *pmeth,
|
||||||
|
int (**digestsign) (EVP_MD_CTX *ctx,
|
||||||
|
unsigned char *sig,
|
||||||
|
size_t *siglen,
|
||||||
|
const unsigned char *tbs,
|
||||||
|
size_t tbslen));
|
||||||
|
|
||||||
|
void EVP_PKEY_meth_get_digestverify(EVP_PKEY_METHOD *pmeth,
|
||||||
|
int (**digestverify) (EVP_MD_CTX *ctx,
|
||||||
|
const unsigned char *sig,
|
||||||
|
size_t siglen,
|
||||||
|
const unsigned char *tbs,
|
||||||
|
size_t tbslen));
|
||||||
|
|
||||||
void EVP_PKEY_meth_get_check(const EVP_PKEY_METHOD *pmeth,
|
void EVP_PKEY_meth_get_check(const EVP_PKEY_METHOD *pmeth,
|
||||||
int (**pcheck) (EVP_PKEY *pkey));
|
int (**pcheck) (EVP_PKEY *pkey));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2021 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
#ifndef HEADER_EVPERR_H
|
#ifndef HEADER_EVPERR_H
|
||||||
# define HEADER_EVPERR_H
|
# define HEADER_EVPERR_H
|
||||||
|
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
@@ -20,11 +22,14 @@ int ERR_load_EVP_strings(void);
|
|||||||
* EVP function codes.
|
* EVP function codes.
|
||||||
*/
|
*/
|
||||||
# define EVP_F_AESNI_INIT_KEY 165
|
# define EVP_F_AESNI_INIT_KEY 165
|
||||||
|
# define EVP_F_AESNI_XTS_INIT_KEY 207
|
||||||
# define EVP_F_AES_GCM_CTRL 196
|
# define EVP_F_AES_GCM_CTRL 196
|
||||||
# define EVP_F_AES_INIT_KEY 133
|
# define EVP_F_AES_INIT_KEY 133
|
||||||
# define EVP_F_AES_OCB_CIPHER 169
|
# define EVP_F_AES_OCB_CIPHER 169
|
||||||
# define EVP_F_AES_T4_INIT_KEY 178
|
# define EVP_F_AES_T4_INIT_KEY 178
|
||||||
|
# define EVP_F_AES_T4_XTS_INIT_KEY 208
|
||||||
# define EVP_F_AES_WRAP_CIPHER 170
|
# define EVP_F_AES_WRAP_CIPHER 170
|
||||||
|
# define EVP_F_AES_XTS_INIT_KEY 209
|
||||||
# define EVP_F_ALG_MODULE_INIT 177
|
# define EVP_F_ALG_MODULE_INIT 177
|
||||||
# define EVP_F_ARIA_CCM_INIT_KEY 175
|
# define EVP_F_ARIA_CCM_INIT_KEY 175
|
||||||
# define EVP_F_ARIA_GCM_CTRL 197
|
# define EVP_F_ARIA_GCM_CTRL 197
|
||||||
@@ -115,6 +120,7 @@ int ERR_load_EVP_strings(void);
|
|||||||
# define EVP_F_PKEY_SET_TYPE 158
|
# define EVP_F_PKEY_SET_TYPE 158
|
||||||
# define EVP_F_RC2_MAGIC_TO_METH 109
|
# define EVP_F_RC2_MAGIC_TO_METH 109
|
||||||
# define EVP_F_RC5_CTRL 125
|
# define EVP_F_RC5_CTRL 125
|
||||||
|
# define EVP_F_R_32_12_16_INIT_KEY 242
|
||||||
# define EVP_F_S390X_AES_GCM_CTRL 201
|
# define EVP_F_S390X_AES_GCM_CTRL 201
|
||||||
# define EVP_F_UPDATE 173
|
# define EVP_F_UPDATE 173
|
||||||
|
|
||||||
@@ -124,6 +130,7 @@ int ERR_load_EVP_strings(void);
|
|||||||
# define EVP_R_AES_KEY_SETUP_FAILED 143
|
# define EVP_R_AES_KEY_SETUP_FAILED 143
|
||||||
# define EVP_R_ARIA_KEY_SETUP_FAILED 176
|
# define EVP_R_ARIA_KEY_SETUP_FAILED 176
|
||||||
# define EVP_R_BAD_DECRYPT 100
|
# define EVP_R_BAD_DECRYPT 100
|
||||||
|
# define EVP_R_BAD_KEY_LENGTH 195
|
||||||
# define EVP_R_BUFFER_TOO_SMALL 155
|
# define EVP_R_BUFFER_TOO_SMALL 155
|
||||||
# define EVP_R_CAMELLIA_KEY_SETUP_FAILED 157
|
# define EVP_R_CAMELLIA_KEY_SETUP_FAILED 157
|
||||||
# define EVP_R_CIPHER_PARAMETER_ERROR 122
|
# define EVP_R_CIPHER_PARAMETER_ERROR 122
|
||||||
@@ -151,6 +158,7 @@ int ERR_load_EVP_strings(void);
|
|||||||
# define EVP_R_INPUT_NOT_INITIALIZED 111
|
# define EVP_R_INPUT_NOT_INITIALIZED 111
|
||||||
# define EVP_R_INVALID_DIGEST 152
|
# define EVP_R_INVALID_DIGEST 152
|
||||||
# define EVP_R_INVALID_FIPS_MODE 168
|
# define EVP_R_INVALID_FIPS_MODE 168
|
||||||
|
# define EVP_R_INVALID_IV_LENGTH 194
|
||||||
# define EVP_R_INVALID_KEY 163
|
# define EVP_R_INVALID_KEY 163
|
||||||
# define EVP_R_INVALID_KEY_LENGTH 130
|
# define EVP_R_INVALID_KEY_LENGTH 130
|
||||||
# define EVP_R_INVALID_OPERATION 148
|
# define EVP_R_INVALID_OPERATION 148
|
||||||
@@ -169,6 +177,7 @@ int ERR_load_EVP_strings(void);
|
|||||||
# define EVP_R_ONLY_ONESHOT_SUPPORTED 177
|
# define EVP_R_ONLY_ONESHOT_SUPPORTED 177
|
||||||
# define EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE 150
|
# define EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE 150
|
||||||
# define EVP_R_OPERATON_NOT_INITIALIZED 151
|
# define EVP_R_OPERATON_NOT_INITIALIZED 151
|
||||||
|
# define EVP_R_OUTPUT_WOULD_OVERFLOW 184
|
||||||
# define EVP_R_PARTIALLY_OVERLAPPING 162
|
# define EVP_R_PARTIALLY_OVERLAPPING 162
|
||||||
# define EVP_R_PBKDF2_ERROR 181
|
# define EVP_R_PBKDF2_ERROR 181
|
||||||
# define EVP_R_PKEY_APPLICATION_ASN1_METHOD_ALREADY_REGISTERED 179
|
# define EVP_R_PKEY_APPLICATION_ASN1_METHOD_ALREADY_REGISTERED 179
|
||||||
@@ -190,5 +199,6 @@ int ERR_load_EVP_strings(void);
|
|||||||
# define EVP_R_UNSUPPORTED_SALT_TYPE 126
|
# define EVP_R_UNSUPPORTED_SALT_TYPE 126
|
||||||
# define EVP_R_WRAP_MODE_NOT_ALLOWED 170
|
# define EVP_R_WRAP_MODE_NOT_ALLOWED 170
|
||||||
# define EVP_R_WRONG_FINAL_BLOCK_LENGTH 109
|
# define EVP_R_WRONG_FINAL_BLOCK_LENGTH 109
|
||||||
|
# define EVP_R_XTS_DUPLICATED_KEYS 183
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_KDFERR_H
|
#ifndef HEADER_KDFERR_H
|
||||||
# define HEADER_KDFERR_H
|
# define HEADER_KDFERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2020 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -120,9 +120,8 @@ void OPENSSL_LH_node_usage_stats_bio(const OPENSSL_LHASH *lh, BIO *out);
|
|||||||
|
|
||||||
# define DEFINE_LHASH_OF(type) \
|
# define DEFINE_LHASH_OF(type) \
|
||||||
LHASH_OF(type) { union lh_##type##_dummy { void* d1; unsigned long d2; int d3; } dummy; }; \
|
LHASH_OF(type) { union lh_##type##_dummy { void* d1; unsigned long d2; int d3; } dummy; }; \
|
||||||
static ossl_inline LHASH_OF(type) * \
|
static ossl_unused ossl_inline LHASH_OF(type) *lh_##type##_new(unsigned long (*hfn)(const type *), \
|
||||||
lh_##type##_new(unsigned long (*hfn)(const type *), \
|
int (*cfn)(const type *, const type *)) \
|
||||||
int (*cfn)(const type *, const type *)) \
|
|
||||||
{ \
|
{ \
|
||||||
return (LHASH_OF(type) *) \
|
return (LHASH_OF(type) *) \
|
||||||
OPENSSL_LH_new((OPENSSL_LH_HASHFUNC)hfn, (OPENSSL_LH_COMPFUNC)cfn); \
|
OPENSSL_LH_new((OPENSSL_LH_HASHFUNC)hfn, (OPENSSL_LH_COMPFUNC)cfn); \
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* WARNING: do not edit!
|
* WARNING: do not edit!
|
||||||
* Generated by crypto/objects/objects.pl
|
* Generated by crypto/objects/objects.pl
|
||||||
*
|
*
|
||||||
* Copyright 2000-2019 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 2000-2021 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
* in the file LICENSE in the source distribution or at
|
* in the file LICENSE in the source distribution or at
|
||||||
@@ -1290,12 +1290,12 @@
|
|||||||
#define OBJ_ms_efs 1L,3L,6L,1L,4L,1L,311L,10L,3L,4L
|
#define OBJ_ms_efs 1L,3L,6L,1L,4L,1L,311L,10L,3L,4L
|
||||||
|
|
||||||
#define SN_ms_smartcard_login "msSmartcardLogin"
|
#define SN_ms_smartcard_login "msSmartcardLogin"
|
||||||
#define LN_ms_smartcard_login "Microsoft Smartcardlogin"
|
#define LN_ms_smartcard_login "Microsoft Smartcard Login"
|
||||||
#define NID_ms_smartcard_login 648
|
#define NID_ms_smartcard_login 648
|
||||||
#define OBJ_ms_smartcard_login 1L,3L,6L,1L,4L,1L,311L,20L,2L,2L
|
#define OBJ_ms_smartcard_login 1L,3L,6L,1L,4L,1L,311L,20L,2L,2L
|
||||||
|
|
||||||
#define SN_ms_upn "msUPN"
|
#define SN_ms_upn "msUPN"
|
||||||
#define LN_ms_upn "Microsoft Universal Principal Name"
|
#define LN_ms_upn "Microsoft User Principal Name"
|
||||||
#define NID_ms_upn 649
|
#define NID_ms_upn 649
|
||||||
#define OBJ_ms_upn 1L,3L,6L,1L,4L,1L,311L,20L,2L,3L
|
#define OBJ_ms_upn 1L,3L,6L,1L,4L,1L,311L,20L,2L,3L
|
||||||
|
|
||||||
@@ -4280,7 +4280,7 @@
|
|||||||
|
|
||||||
#define SN_id_tc26_wrap_gostr3412_2015_kuznyechik_kexp15 "id-tc26-wrap-gostr3412-2015-kuznyechik-kexp15"
|
#define SN_id_tc26_wrap_gostr3412_2015_kuznyechik_kexp15 "id-tc26-wrap-gostr3412-2015-kuznyechik-kexp15"
|
||||||
#define NID_id_tc26_wrap_gostr3412_2015_kuznyechik_kexp15 1183
|
#define NID_id_tc26_wrap_gostr3412_2015_kuznyechik_kexp15 1183
|
||||||
#define OBJ_id_tc26_wrap_gostr3412_2015_kuznyechik_kexp15 OBJ_id_tc26_wrap_gostr3412_2015_magma,1L
|
#define OBJ_id_tc26_wrap_gostr3412_2015_kuznyechik_kexp15 OBJ_id_tc26_wrap_gostr3412_2015_kuznyechik,1L
|
||||||
|
|
||||||
#define SN_id_tc26_constants "id-tc26-constants"
|
#define SN_id_tc26_constants "id-tc26-constants"
|
||||||
#define NID_id_tc26_constants 994
|
#define NID_id_tc26_constants 994
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Generated by util/mkerr.pl DO NOT EDIT
|
* Generated by util/mkerr.pl DO NOT EDIT
|
||||||
* Copyright 1995-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 1995-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
#ifndef HEADER_OBJERR_H
|
#ifndef HEADER_OBJERR_H
|
||||||
# define HEADER_OBJERR_H
|
# define HEADER_OBJERR_H
|
||||||
|
|
||||||
|
# ifndef HEADER_SYMHACKS_H
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifdef __cplusplus
|
# ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
# endif
|
# endif
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2000-2018 The OpenSSL Project Authors. All Rights Reserved.
|
* Copyright 2000-2019 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the OpenSSL license (the "License"). You may not use
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
* this file except in compliance with the License. You can obtain a copy
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
@@ -123,7 +123,7 @@ typedef struct ocsp_service_locator_st OCSP_SERVICELOC;
|
|||||||
(char *(*)())d2i_OCSP_REQUEST,PEM_STRING_OCSP_REQUEST, \
|
(char *(*)())d2i_OCSP_REQUEST,PEM_STRING_OCSP_REQUEST, \
|
||||||
bp,(char **)(x),cb,NULL)
|
bp,(char **)(x),cb,NULL)
|
||||||
|
|
||||||
# define PEM_read_bio_OCSP_RESPONSE(bp,x,cb)(OCSP_RESPONSE *)PEM_ASN1_read_bio(\
|
# define PEM_read_bio_OCSP_RESPONSE(bp,x,cb) (OCSP_RESPONSE *)PEM_ASN1_read_bio(\
|
||||||
(char *(*)())d2i_OCSP_RESPONSE,PEM_STRING_OCSP_RESPONSE, \
|
(char *(*)())d2i_OCSP_RESPONSE,PEM_STRING_OCSP_RESPONSE, \
|
||||||
bp,(char **)(x),cb,NULL)
|
bp,(char **)(x),cb,NULL)
|
||||||
|
|
||||||
@@ -229,8 +229,8 @@ int OCSP_request_verify(OCSP_REQUEST *req, STACK_OF(X509) *certs,
|
|||||||
int OCSP_parse_url(const char *url, char **phost, char **pport, char **ppath,
|
int OCSP_parse_url(const char *url, char **phost, char **pport, char **ppath,
|
||||||
int *pssl);
|
int *pssl);
|
||||||
|
|
||||||
int OCSP_id_issuer_cmp(OCSP_CERTID *a, OCSP_CERTID *b);
|
int OCSP_id_issuer_cmp(const OCSP_CERTID *a, const OCSP_CERTID *b);
|
||||||
int OCSP_id_cmp(OCSP_CERTID *a, OCSP_CERTID *b);
|
int OCSP_id_cmp(const OCSP_CERTID *a, const OCSP_CERTID *b);
|
||||||
|
|
||||||
int OCSP_request_onereq_count(OCSP_REQUEST *req);
|
int OCSP_request_onereq_count(OCSP_REQUEST *req);
|
||||||
OCSP_ONEREQ *OCSP_request_onereq_get0(OCSP_REQUEST *req, int i);
|
OCSP_ONEREQ *OCSP_request_onereq_get0(OCSP_REQUEST *req, int i);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user