Compare commits

..

129 Commits

Author SHA1 Message Date
Cameron Gutman 964d2ce59c Version 9.6 2020-06-18 23:11:35 -07:00
Cameron Gutman dc52684cbc Update moonlight-common-c to fix QoS-related connection issues 2020-06-12 22:08:01 -07:00
Cameron Gutman 191bedc56f Improve behavior and description of small box art checkbox 2020-06-11 22:01:48 -07:00
Cameron Gutman 47b2ace7fd New app grid UI 2020-06-11 21:51:07 -07:00
Cameron Gutman 9fb7359a3e Use startAnimation() instead of setAnimation() 2020-06-11 21:47:28 -07:00
Cameron Gutman 4a5de26406 Remove the small PC grid UI 2020-06-11 21:32:39 -07:00
Cameron Gutman 6fa18e126f Remove list view in preparation for grid redesign 2020-06-11 21:21:37 -07:00
Cameron Gutman 1149002e0c Improve PC and game details dialogs 2020-06-11 20:36:59 -07:00
Cameron Gutman d704cb0b50 Use SoftReferences instead of WeakReferences for the eviction cache 2020-06-11 19:10:43 -07:00
Cameron Gutman d59e5ae9cf Store the original bitmap dimensions for the box art 2020-06-11 19:08:25 -07:00
Cameron Gutman 4587c1550d Cache WeakReferences to our box art bitmaps after LRU evictions 2020-06-10 23:13:07 -07:00
Cameron Gutman b5bd329ada Fade in the box art when loading from the network 2020-06-10 22:52:37 -07:00
Cameron Gutman beccd7a4ac Fade in the box art as we load it 2020-06-10 22:37:54 -07:00
Cameron Gutman 61262fa939 Refactor grid adapters for new grid UI 2020-06-10 22:13:02 -07:00
Cameron Gutman 7c6b006631 Remove OSC rumble option if a vibrator isn't present 2020-06-10 21:15:21 -07:00
Cameron Gutman dbd149354a Change "crashes" to "instability" 2020-06-10 21:09:24 -07:00
Cameron Gutman 4306ba5004 Add a mapping for the Nintendo Switch Pro controller
Fixes #842
2020-06-10 21:05:08 -07:00
Cameron Gutman 6de370b82f Update for Android 11 2020-06-10 20:31:32 -07:00
Cameron Gutman 45781666b8 Disable the latency toast by default
It causes crashes on the MiBox
2020-06-06 18:24:34 -07:00
Cameron Gutman 538231eb6f Attempt to appease Amazon content review 2020-06-06 17:53:09 -07:00
Cameron Gutman eb74f87f2c Move PiP and unlock FPS options out of basic settings 2020-06-06 17:44:38 -07:00
Cameron Gutman 59d71ffdcf Don't show PiP option on devices where PiP is disabled 2020-06-06 17:32:26 -07:00
Cameron Gutman d1b93d4011 Remove vibration option if the device can't vibrate 2020-06-06 17:25:01 -07:00
Cameron Gutman d8ddf2e740 Update NDK for Travis CI 2020-05-28 22:19:58 -07:00
Cameron Gutman 581327dc8e Improve resolution preference storage to remove 16:9 assumptions 2020-05-28 22:05:57 -07:00
Cameron Gutman 76e4512a0c Update for Android Studio 4.0 2020-05-28 21:50:28 -07:00
Cameron Gutman efdd55beca Add Download links 2020-05-27 19:47:30 -07:00
Cameron Gutman 2c115649b9 Update README 2020-05-27 19:44:56 -07:00
Cameron Gutman 2ddcc31a93 Update metadata for Quadro streaming 2020-05-27 18:34:59 -07:00
Cameron Gutman 3bcce5b749 Version 9.5.1 2020-05-27 18:34:26 -07:00
Cameron Gutman 80dac27214 Update moonlight-common-c 2020-05-27 00:02:33 -07:00
Cameron Gutman 4a1177d048 Use a better workaround for the GFE 3.20.3 high FPS bug 2020-05-25 19:28:00 -07:00
Cameron Gutman 4725d8f270 Revert "Disable SOPS for streams over 60 FPS for GFE 3.20.3"
This reverts commit 63072aa8e1.
2020-05-25 19:24:33 -07:00
Zero O 07b3528515 Update strings.xml (#833)
update translation
2020-05-20 19:27:47 -07:00
Zero O d2d1b1ea26 Update strings.xml (#834)
update translation
2020-05-20 19:27:32 -07:00
Cameron Gutman 232b897abc Version 9.5 2020-05-16 21:40:41 -07:00
Cameron Gutman efd076bc6c Ignore absolute touch events from outside the stream view 2020-05-12 00:20:07 -07:00
Cameron Gutman cc877480ff Add an option for absolute touch mode 2020-05-11 23:53:49 -07:00
Christoph Papke 363145a284 Optimize button mapping for 8BitDo controllers (#826)
* Optimize button mapping for 8BitDo controllers #825
2020-05-05 16:04:31 -07:00
Cameron Gutman 755571ad33 Switch on-screen control buttons when flip face buttons is enabled 2020-05-04 22:23:03 -07:00
Eero Kelly 39edb55721 Add option to invert A/B X/Y (#824)
* Add option to invert A/B X/Y

* Remove redundant prefConfig
2020-05-04 22:10:35 -07:00
Cameron Gutman 15aa7ecc2e Add a friendly error message when no video traffic is received 2020-05-01 21:54:26 -07:00
Cameron Gutman ce9e91153e Add special error text for the -1 launch error code 2020-04-25 16:10:44 -07:00
Cameron Gutman 9ee0a46606 Add new init packet to switch out of BT mode 2020-04-24 17:47:31 -07:00
Cameron Gutman 20dc351f4c Fix parsing rare GFE status code of 0xFFFFFFFF 2020-04-23 18:47:01 -07:00
Cameron Gutman c30c54d562 Version 9.2.1 2020-04-23 18:40:40 -07:00
Cameron Gutman 45ff51c0d2 Fix mouse jumping on Shield devices when clicking or scrolling 2020-04-23 00:13:19 -07:00
Cameron Gutman 5b86e99138 Improve dead zone precision for stylus input 2020-04-22 22:46:05 -07:00
Cameron Gutman 0c72910eb7 Fix tap location for styluses without hover support 2020-04-22 22:00:25 -07:00
Cameron Gutman 3b0f485b41 Version 9.2 2020-04-20 14:44:38 -07:00
Cameron Gutman 2be2c95212 Avoid crashing if we get an invalid status code back from GFE 2020-04-18 22:46:21 -07:00
Cameron Gutman e7aeeb8bd5 Fix one more place where the HTTP error code was lost 2020-04-18 18:03:29 -07:00
Cameron Gutman 73df93f86a Display the error code correctly for HTTPS errors 2020-04-18 17:47:27 -07:00
Cameron Gutman 9cd4d5e2aa Implement a post-tap deadzone for stylus input 2020-04-18 01:03:49 -07:00
Cameron Gutman c3b81554f4 Add absolute mouse support for styluses and mice prior to Oreo 2020-04-18 00:02:36 -07:00
Cameron Gutman 6f79c52fc5 Plumb sendMousePosition() through to moonlight-common-c 2020-04-17 22:37:09 -07:00
Cameron Gutman 29bc3e022b Update AGP to 3.6.3 2020-04-17 22:36:19 -07:00
Cameron Gutman 7d03203d83 Add special Start and Select mappings for the ROG Kunai 2020-04-15 23:47:09 -07:00
Cameron Gutman 11dde835d1 Version 9.1 2020-04-14 22:29:41 -07:00
Cameron Gutman 52c47c288c Disable the 7.1 surround sound option prior to Lollipop 2020-04-12 12:28:42 -07:00
Cameron Gutman 63072aa8e1 Disable SOPS for streams over 60 FPS for GFE 3.20.3 2020-04-12 12:13:38 -07:00
Cameron Gutman 4cca3ac922 Update moonlight-common-c to avoid termination delay on GFE 3.20.3 2020-04-12 12:13:04 -07:00
Cameron Gutman 604bc1ec11 Add Romanian translation from KiralyCraft on Discord 2020-04-12 12:04:04 -07:00
Cameron Gutman 5d7fbf3195 Fix indentation of arrays.xml 2020-04-10 22:17:28 -07:00
Zero O 8c56e6f0d4 Update arrays.xml (#813)
translation update
2020-04-10 22:12:18 -07:00
Zero O 2069be7932 Update arrays.xml (#814)
translation update
2020-04-10 22:11:56 -07:00
Zero O 9c1c2991a9 Update strings.xml (#812)
translation update
2020-04-10 21:13:10 -07:00
Zero O 81dabf2713 Update strings.xml (#811)
translation update
2020-04-10 21:12:48 -07:00
Cameron Gutman 27520cb77e Use GetPrimitiveArrayCritical() for audio data to avoid extra copies 2020-04-09 19:12:09 -07:00
Cameron Gutman f555d3dae0 Version 9.0 2020-04-07 19:42:47 -07:00
Cameron Gutman 70f1a2cacb Fix 7.1 AudioTrack initialization on pre-Lollipop devices 2020-04-07 19:29:07 -07:00
Cameron Gutman 7f15aaa2e5 Update to AGP 3.6.2 2020-04-07 19:22:02 -07:00
Cameron Gutman e5726205c4 7.1 surround sound is supported now 2020-04-07 19:21:45 -07:00
Cameron Gutman 07fabc0663 Fix CheckJNI abort with rumble values greater than 0x7FFF 2020-04-07 19:21:24 -07:00
Cameron Gutman 800f97ae85 Remove translations for old 5.1 surround sound option 2020-04-04 10:15:03 -07:00
bubuleur 3ee5b284e1 Update french "summary_audio_config_list" (#809) 2020-04-04 10:08:42 -07:00
bubuleur c0389f0da9 Update french "audio_config_names" (#808) 2020-04-04 10:08:18 -07:00
bubuleur a7a4d7ded5 Update french 2 (#807)
* Update french 2

* Update strings.xml
2020-04-03 18:13:30 -07:00
bubuleur 87cd974b79 Update French 1 (#806)
* Update French 1

* Update arrays.xml
2020-04-03 18:10:34 -07:00
Cameron Gutman 7faaac31ff Use EF instead of CS7 for DSCP on ENet traffic 2020-04-03 18:04:04 -07:00
Cameron Gutman 7386eb2a78 Add support for 7.1 surround sound 2020-04-03 18:03:01 -07:00
Cameron Gutman 49a1524f4f Refactor audio configuration in preparation for 7.1 surround sound 2020-04-03 17:47:57 -07:00
Cameron Gutman c957b8b06b Version 8.12 2020-03-29 16:46:42 -07:00
Cameron Gutman a3a6e14d80 Reduce retransmission delay on packet loss and enable QoS marking on ENet traffic 2020-03-29 16:31:23 -07:00
Cameron Gutman 7231f5468b Version 8.11 2020-03-25 00:07:53 -07:00
Cameron Gutman 4dfb0d7220 Fix crash during crash report generation 2020-03-22 13:48:17 -07:00
Cameron Gutman 2f4f53b048 Fix mouse back button closing the app with mouseNavButtons enabled 2020-03-21 15:34:03 -07:00
Cameron Gutman b6e8389544 Fix incorrect exception handling in JNI code 2020-03-21 14:30:31 -07:00
Cameron Gutman d113878613 Use current display refresh rate only for non-TV devices 2020-03-21 13:43:59 -07:00
Cameron Gutman f7ed7e06db Revert "Calculate FPS using the actual display refresh rate rather than the requested one"
This breaks refresh rate detection on the Shield Android TV.

This reverts commit af5e7a0e33.
2020-03-21 13:31:48 -07:00
Cameron Gutman 977a1d4a3c Fix IllegalArgumentException when trying to repin a disabled shortcut 2020-03-21 13:25:55 -07:00
Cameron Gutman eefc08db47 Use 10 ms audio samples on low bandwidth connections 2020-03-21 01:01:45 -07:00
Cameron Gutman ab2b1663d3 Minor tweaks and fixes to OSC opacity options 2020-03-21 00:54:31 -07:00
gotoAndDie 04b8a718e3 Add opacity settings to on-screen controls (#798)
* Restore resize controls, Make buttons oval

* Create new default configuration

* Split Configuration Mode into separate Move and Resize modes

* Add transparency setting for on-screen buttons

* Updated translations for on-screen controls

Co-authored-by: Leo <chun.huang@student.manchester.ac.uk>
2020-03-21 00:41:27 -07:00
Cameron Gutman 37cf260ba6 Merge pull request #799 from gotoAndDie/rt-onefinger
Allow RT/LT and A/B/X/Y/LB/RB to be triggered together with one finger
2020-03-21 00:23:24 -07:00
Cameron Gutman 8f91fe4cd1 Revert "Repeat key down events are needed for proper key repeating"
This key repeat filtering seems to be needed now. See #800.

This reverts commit 53dccbde2a.
2020-03-20 23:49:52 -07:00
Leo 9246ad412f Make it possible to press the RT button and the other buttons with the same finger 2020-03-13 18:48:50 +00:00
Cameron Gutman 1ccbbdd4fb Version 8.10 2020-03-08 19:48:53 -07:00
Cameron Gutman 16cf37994d Only suppress duplicate d-pad events if the hat has received input. Fixes #796 2020-03-04 18:48:14 -08:00
Cameron Gutman 01e84624c2 Remove stale moonlight-common reference from settings.gradle 2020-03-03 00:13:48 -08:00
Cameron Gutman 939cd7cf70 Update OkHttp to 3.12.10 2020-03-02 22:49:44 -08:00
Cameron Gutman 4b11603035 Fix back button on Shield Portable and standardize external/internal classification 2020-03-02 22:47:47 -08:00
Cameron Gutman ca18b6b052 Update to AGP 3.6.1 2020-03-01 13:12:04 -08:00
Cameron Gutman 3d0d19e561 Pass-through back button on external devices that don't look like gamepads 2020-03-01 12:45:00 -08:00
Cameron Gutman ae463a8735 Emulated button combos must not be pressed with other buttons 2020-02-26 20:38:53 -08:00
Cameron Gutman 7e797829ae Also destroy the mouse emulation timer on device disconnect 2020-02-26 20:29:28 -08:00
Cameron Gutman 431ed6bc5d Cancel the mouse emulation timer when the stream ends 2020-02-26 20:18:11 -08:00
Cameron Gutman e9bb711c42 Add Start+Back+LB+RB combo for disconnecting the session 2020-02-26 19:54:53 -08:00
Cameron Gutman 623bc5c156 Fix check for gamepad buttons. Fixes #788 2020-02-26 19:19:43 -08:00
Cameron Gutman cfefef4619 Downgrade OkHTTP to 3.12.8 due to square/okhttp#5826 2020-02-25 22:40:17 -08:00
Cameron Gutman 4a9a881c1f Add missing else block 2020-02-25 22:26:52 -08:00
Cameron Gutman 13a06d585c Update dependencies 2020-02-25 20:49:30 -08:00
Cameron Gutman 1c8ad64da0 Only set KEY_FRAME_RATE on M+ to reduce compatibility risk 2020-02-25 20:24:18 -08:00
Cameron Gutman 1d8925de57 Fix NDK version in Travis CI build 2020-02-25 00:38:41 -08:00
Cameron Gutman 0eb7e779b8 Update Travis CI to build-tools-29.0.3 2020-02-25 00:25:15 -08:00
Cameron Gutman a4b86eefe2 Change errorCode from long to int to fix 32-bit platforms 2020-02-24 23:24:22 -08:00
Cameron Gutman 902a58bc70 Improve video decoder init failure message 2020-02-24 23:23:23 -08:00
Cameron Gutman a34a44f29a Fix crash on Android 5.0 and earlier 2020-02-24 22:05:26 -08:00
Cameron Gutman 454fe80172 Update Gradle and AGP for AS 3.6.0 2020-02-24 21:49:14 -08:00
Cameron Gutman 81b6a8a311 Set the vendor.qti-ext-dec-low-latency.enable Qualcomm vendor extension 2020-02-22 17:06:32 -08:00
Cameron Gutman 3011a5bad7 Use the unmodified FPS value when sending the launch request 2020-02-22 01:28:41 -08:00
Cameron Gutman dcb7be3acd Use the original FPS value for KEY_FRAME_RATE 2020-02-22 01:18:11 -08:00
Cameron Gutman 68a6b510b1 Set KEY_FRAME_RATE for devices where KEY_OPERATING_RATE silently fails 2020-02-22 01:05:26 -08:00
Cameron Gutman dca3e89303 Log configured MediaFormat and achievable FPS ranges 2020-02-22 01:04:18 -08:00
Cameron Gutman bae6fef588 Log the actual input and output formats 2020-02-21 22:02:37 -08:00
Cameron Gutman 37f65e43a5 Add error code on connection failure dialog 2020-02-21 22:01:12 -08:00
Cameron Gutman 8c910101c7 Fix Lint errors on API level 16 2020-02-19 23:53:44 -08:00
Cameron Gutman 112d9c41eb Use KEY_LOW_LATENCY to request low-latency decoding on Android R 2020-02-19 23:40:06 -08:00
Cameron Gutman c91d1097f6 Set preferMinimalPostProcessing on Android R 2020-02-19 23:29:37 -08:00
90 changed files with 1989 additions and 873 deletions
+6 -3
View File
@@ -8,8 +8,11 @@ android:
components:
- tools
- platform-tools
- build-tools-29.0.1
- android-29
- build-tools-30.0.0
- android-30
before_install:
- sdkmanager --list
install:
- yes | sdkmanager "ndk-bundle"
- yes | sdkmanager "ndk;21.0.6113669"
+10 -39
View File
@@ -2,55 +2,26 @@
[![Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-android.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-android)
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
[Moonlight for Android](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
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.
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
Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed project information, setup guide, or troubleshooting steps.
* Streams any of your games from your PC to your Android device
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network
## Installation
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [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
## Downloads
* [Google Play Store](https://play.google.com/store/apps/details?id=com.limelight)
* [Amazon App Store](https://www.amazon.com/gp/product/B00JK4MFN2)
* [F-Droid](https://f-droid.org/packages/com.limelight)
* [APK](https://github.com/moonlight-stream/moonlight-android/releases)
## Building
* Install Android Studio and the Android NDK
* Run git submodule update --init --recursive from within moonlight-android/
* In moonlight-android/, create a file called local.properties. Add an ndk.dir= property to the local.properties file and set it equal to your NDK directory.
* Build the APK using Android Studio
* Build the APK using Android Studio or gradle
## Authors
+8 -8
View File
@@ -1,14 +1,14 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
targetSdkVersion 30
versionName "8.9"
versionCode = 211
versionName "9.6"
versionCode = 231
}
flavorDimensions "root"
@@ -114,10 +114,10 @@ android {
}
dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.62'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.62'
implementation 'org.bouncycastle:bcprov-jdk15on:1.64'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.64'
implementation 'org.jcodec:jcodec:0.2.3'
implementation 'com.squareup.okhttp3:okhttp:3.12.3'
implementation 'com.squareup.okio:okio:1.17.4'
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
implementation 'com.squareup.okio:okio:1.17.5'
implementation 'org.jmdns:jmdns:3.5.5'
}
+2 -1
View File
@@ -120,7 +120,8 @@
android:resizeableActivity="true"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:theme="@style/StreamTheme">
android:theme="@style/StreamTheme"
android:preferMinimalPostProcessing="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.AppView" />
+9 -5
View File
@@ -434,8 +434,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
return true;
case VIEW_DETAILS_ID:
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details), app.app.toString(), false);
return true;
case CREATE_SHORTCUT_ID:
@@ -517,6 +516,12 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
// We could have a leftover shortcut from last time this PC was paired
// or if this app was removed then added again. Enable those shortcuts
// again if present.
shortcutHelper.enableAppShortcut(computer, app);
updated = true;
}
}
@@ -559,9 +564,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view);
return PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view;
}
@Override
+261 -115
View File
@@ -6,9 +6,11 @@ import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.binding.input.capture.InputCaptureManager;
import com.limelight.binding.input.capture.InputCaptureProvider;
import com.limelight.binding.input.TouchContext;
import com.limelight.binding.input.touch.AbsoluteTouchContext;
import com.limelight.binding.input.touch.RelativeTouchContext;
import com.limelight.binding.input.driver.UsbDriverService;
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.video.CrashListener;
import com.limelight.binding.video.MediaCodecDecoderRenderer;
@@ -43,6 +45,7 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
@@ -60,6 +63,7 @@ import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.View.OnGenericMotionListener;
@@ -73,6 +77,7 @@ import android.widget.TextView;
import android.widget.Toast;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -84,8 +89,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
PerfOverlayListener
{
private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE;
private int lastButtonState = 0;
// Only 2 touches are supported
@@ -95,6 +98,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private static final int REFERENCE_HORIZ_RES = 1280;
private static final int REFERENCE_VERT_RES = 720;
private static final int STYLUS_DOWN_DEAD_ZONE_DELAY = 100;
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 ControllerHandler controllerHandler;
@@ -116,6 +125,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean grabbedInput = true;
private boolean grabComboDown = false;
private StreamView streamView;
private long lastAbsTouchUpTime = 0;
private long lastAbsTouchDownTime = 0;
private float lastAbsTouchUpX, lastAbsTouchUpY;
private float lastAbsTouchDownX, lastAbsTouchDownY;
private boolean isHidingOverlays;
private TextView notificationOverlayView;
@@ -200,11 +213,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
prefConfig = PreferenceConfiguration.readPreferences(this);
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && prefConfig.stretchVideo) {
if (prefConfig.stretchVideo) {
// Allow the activity to layout under notches if the fill-screen option
// was turned on by the user
getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
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
@@ -226,7 +245,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
@Override
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
return handleMotionEvent(motionEvent);
return handleMotionEvent(view, motionEvent);
}
});
}
@@ -398,6 +417,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// 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 chosenFrameRate = prefConfig.fps;
if (!prefConfig.disableFrameDrop || prefConfig.unlockFps) {
if (Build.DEVICE.equals("coral") || Build.DEVICE.equals("flame")) {
// HACK: Pixel 4 (XL) ignores the preferred display mode and lowers refresh rate,
@@ -423,8 +443,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Use the old rendering strategy on these broken devices
decoderRenderer.enableLegacyFrameDropRendering();
} else {
prefConfig.fps = roundedRefreshRate - 1;
LimeLog.info("Adjusting FPS target for screen to " + prefConfig.fps);
chosenFrameRate = roundedRefreshRate - 1;
LimeLog.info("Adjusting FPS target for screen to " + chosenFrameRate);
}
}
}
@@ -436,7 +456,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setLaunchRefreshRate(prefConfig.fps)
.setRefreshRate(chosenFrameRate)
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
.setBitrate(prefConfig.bitrate)
.setEnableSops(prefConfig.enableSops)
@@ -450,9 +471,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.setEnableHdr(willStreamHdr)
.setAttachedGamepadMask(gamepadMask)
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
.setAudioConfiguration(prefConfig.enable51Surround ?
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
MoonBridge.AUDIO_CONFIGURATION_STEREO)
.setAudioConfiguration(prefConfig.audioConfiguration)
.build();
// Initialize the connection
@@ -464,9 +483,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i,
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
streamView);
if (!prefConfig.touchscreenTrackpad) {
touchContextMap[i] = new AbsoluteTouchContext(conn, i, streamView);
}
else {
touchContextMap[i] = new RelativeTouchContext(conn, i,
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
streamView);
}
}
// Use sustained performance mode on N+ to ensure consistent
@@ -595,6 +619,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private float prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
float displayRefreshRate;
// On M, we can explicitly set the optimal display mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -638,6 +663,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
displayRefreshRate = bestMode.getRefreshRate();
}
// On L, we can at least tell the OS that we want a refresh rate
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -658,10 +684,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
displayRefreshRate = bestRefreshRate;
}
else {
// Otherwise, the active display refresh rate is just
// whatever is currently in use.
displayRefreshRate = display.getRefreshRate();
}
// Enable HDMI ALLM (game mode) on Android R
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowLayoutParams.preferMinimalPostProcessing = true;
}
// Apply the display mode change
@@ -698,9 +731,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
}
// Use the actual refresh rate of the display, since the preferred refresh rate or mode
// may not actually be applied (ex: Pixel 4 with Smooth Display disabled).
return getWindowManager().getDefaultDisplay().getRefreshRate();
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION) ||
getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
// TVs may take a few moments to switch refresh rates, and we can probably assume
// it will be eventually activated.
// TODO: Improve this
return displayRefreshRate;
}
else {
// Use the actual refresh rate of the display, since the preferred refresh rate or mode
// may not actually be applied (ex: Pixel 4 with Smooth Display disabled).
return getWindowManager().getDefaultDisplay().getRefreshRate();
}
}
@SuppressLint("InlinedApi")
@@ -809,34 +851,36 @@ public class Game extends Activity implements SurfaceHolder.Callback,
displayedFailureDialog = true;
stopConnection();
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
String message = null;
if (averageEndToEndLat > 0) {
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
if (averageDecoderLat > 0) {
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
if (prefConfig.enableLatencyToast) {
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
String message = null;
if (averageEndToEndLat > 0) {
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
if (averageDecoderLat > 0) {
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
}
}
else if (averageDecoderLat > 0) {
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
if (message != null) {
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
message += " [H.265 HDR]";
// Add the video codec to the post-stream toast
if (message != null) {
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
message += " [H.265 HDR]";
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
message += " [H.265]";
}
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) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
// Clear the tombstone count if we terminated normally
@@ -959,11 +1003,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click. This event WILL repeat if
// the right mouse button is held down, so we ignore those.
if (!prefConfig.mouseNavButtons &&
(event.getSource() == InputDevice.SOURCE_MOUSE ||
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
// Send the right mouse button event if mouse back and forward
// are disabled. If they are enabled, handleMotionEvent() will take
// care of this.
if (!prefConfig.mouseNavButtons) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
// Always return true, otherwise the back press will be propagated
// up to the parent and finish the activity.
return true;
}
@@ -987,6 +1039,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
// Eat repeat down events
if (event.getRepeatCount() > 0) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return false;
@@ -1017,11 +1074,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click.
if (!prefConfig.mouseNavButtons &&
(event.getSource() == InputDevice.SOURCE_MOUSE ||
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
// Send the right mouse button event if mouse back and forward
// are disabled. If they are enabled, handleMotionEvent() will take
// care of this.
if (!prefConfig.mouseNavButtons) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
// Always return true, otherwise the back press will be propagated
// up to the parent and finish the activity.
return true;
}
@@ -1079,7 +1144,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Returns true if the event was consumed
private boolean handleMotionEvent(MotionEvent event) {
// NB: View is only present if called from a view callback
private boolean handleMotionEvent(View view, MotionEvent event) {
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return false;
@@ -1093,17 +1159,38 @@ public class Game extends Activity implements SurfaceHolder.Callback,
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
{
// This case is for mice
// This case is for mice and non-finger touch devices
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
(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_ERASER)))
{
int changedButtons = event.getButtonState() ^ lastButtonState;
// Ignore mouse input if we're not capturing from our input source
if (!inputCaptureProvider.isCapturingActive()) {
return false;
// We return true here because otherwise the events may end up causing
// Android to synthesize d-pad events.
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) {
conn.sendMouseMove(deltaX, deltaY);
}
}
else if (view != null) {
// Otherwise send absolute position
updateMousePosition(view, event);
}
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
@@ -1111,13 +1198,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
}
else if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER ||
event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
// On some devices (Galaxy S8 without Oreo pointer capture), we can
// get spurious ACTION_HOVER_ENTER events when right clicking with
// incorrect X and Y coordinates. Just eat this event without processing it.
return true;
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
@@ -1128,8 +1208,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
// Mouse secondary or stylus primary is right click (stylus down is left click)
if ((changedButtons & (MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) {
if ((event.getButtonState() & (MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
else {
@@ -1137,8 +1218,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
// Mouse tertiary or stylus secondary is middle click
if ((changedButtons & (MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_SECONDARY)) != 0) {
if ((event.getButtonState() & (MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_SECONDARY)) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
}
else {
@@ -1166,31 +1248,47 @@ 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));
// Handle stylus presses
if (event.getPointerCount() == 1 && event.getActionIndex() == 0) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
lastAbsTouchDownTime = SystemClock.uptimeMillis();
lastAbsTouchDownX = event.getX(0);
lastAbsTouchDownY = event.getY(0);
// We have to also update the position Android thinks the cursor is at
// in order to avoid jumping when we stop moving or click.
lastMouseX = (int)event.getX();
lastMouseY = (int)event.getY();
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// We get a normal (non-relative) MotionEvent when starting pointer capture to synchronize the
// location of the cursor with our app. We don't want this, so we must discard this event.
lastMouseX = (int)event.getX();
lastMouseY = (int)event.getY();
}
else {
// Don't process the history. We just want the current position now.
updateMousePosition((int)event.getX(), (int)event.getY());
// Stylus is left click
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
lastAbsTouchDownTime = SystemClock.uptimeMillis();
lastAbsTouchDownX = event.getX(0);
lastAbsTouchDownY = event.getY(0);
// Eraser is right click
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
}
else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
lastAbsTouchUpTime = SystemClock.uptimeMillis();
lastAbsTouchUpX = event.getX(0);
lastAbsTouchUpY = event.getY(0);
// Stylus is left click
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
lastAbsTouchUpTime = SystemClock.uptimeMillis();
lastAbsTouchUpX = event.getX(0);
lastAbsTouchUpY = event.getY(0);
// Eraser is right click
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
}
lastButtonState = event.getButtonState();
}
// This case is for touch-based input devices
// This case is for fingers
else
{
if (virtualController != null &&
@@ -1200,6 +1298,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
if (view == null && !prefConfig.touchscreenTrackpad) {
// Absolute touch events should be dropped outside our view.
return true;
}
int actionIndex = event.getActionIndex();
int eventX = (int)event.getX(actionIndex);
@@ -1229,7 +1332,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
context.touchDownEvent(eventX, eventY);
for (TouchContext touchContext : touchContextMap) {
touchContext.setPointerCount(event.getPointerCount());
}
context.touchDownEvent(eventX, eventY, true);
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
@@ -1242,9 +1348,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
context.touchUpEvent(eventX, eventY);
for (TouchContext touchContext : touchContextMap) {
touchContext.setPointerCount(event.getPointerCount() - 1);
}
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
// 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;
case MotionEvent.ACTION_MOVE:
@@ -1276,6 +1385,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case MotionEvent.ACTION_CANCEL:
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
aTouchContext.setPointerCount(0);
}
break;
default:
@@ -1293,48 +1403,67 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onTouchEvent(MotionEvent event) {
return handleMotionEvent(event) || super.onTouchEvent(event);
return handleMotionEvent(null, event) || super.onTouchEvent(event);
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
return handleMotionEvent(event) || super.onGenericMotionEvent(event);
return handleMotionEvent(null, event) || super.onGenericMotionEvent(event);
}
private void updateMousePosition(int eventX, int eventY) {
// Send a mouse move if we already have a mouse location
// and the mouse coordinates change
if (lastMouseX != Integer.MIN_VALUE &&
lastMouseY != Integer.MIN_VALUE &&
!(lastMouseX == eventX && lastMouseY == eventY))
private void updateMousePosition(View view, MotionEvent event) {
// X and Y are already relative to the provided view object
float eventX = event.getX(0);
float eventY = event.getY(0);
if (event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
(event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER ||
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS))
{
int deltaX = eventX - lastMouseX;
int deltaY = eventY - lastMouseY;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_HOVER_ENTER:
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;
// Scale the deltas if the device resolution is different
// than the stream resolution
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)streamView.getWidth()));
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)streamView.getHeight()));
conn.sendMouseMove((short)deltaX, (short)deltaY);
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;
}
}
// Update pointer location for delta calculation next time
lastMouseX = eventX;
lastMouseY = 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), view.getWidth());
eventY = Math.min(Math.max(eventY, 0), view.getHeight());
conn.sendMousePosition((short)eventX, (short)eventY, (short)view.getWidth(), (short)view.getHeight());
}
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
return handleMotionEvent(event);
public boolean onGenericMotion(View view, MotionEvent event) {
return handleMotionEvent(view, event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
return handleMotionEvent(event);
public boolean onTouch(View view, MotionEvent event) {
return handleMotionEvent(view, event);
}
@Override
@@ -1373,7 +1502,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@Override
public void stageFailed(final String stage, final long errorCode) {
public void stageFailed(final String stage, final int errorCode) {
runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -1388,18 +1517,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
Toast.makeText(Game.this, getResources().getText(R.string.video_decoder_init_failed), Toast.LENGTH_LONG).show();
}
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg) + " " + stage, true);
getResources().getString(R.string.conn_error_msg) + " " + stage +" (error "+errorCode+")", true);
}
}
});
}
@Override
public void connectionTerminated(final long errorCode) {
public void connectionTerminated(final int errorCode) {
runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -1416,9 +1545,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Display the error dialog if it was an unexpected termination.
// Otherwise, just finish the activity immediately.
if (errorCode != 0) {
if (errorCode != MoonBridge.ML_ERROR_GRACEFUL_TERMINATION) {
String message;
switch (errorCode) {
case MoonBridge.ML_ERROR_NO_VIDEO_TRAFFIC:
message = getResources().getString(R.string.no_video_received_error);
break;
default:
message = getResources().getString(R.string.conn_terminated_msg);
break;
}
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
message, true);
}
else {
finish();
@@ -1538,6 +1679,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void surfaceCreated(SurfaceHolder holder) {
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
+1 -3
View File
@@ -692,9 +692,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
return R.layout.pc_grid_view;
}
@Override
@@ -64,25 +64,46 @@ public class AndroidAudioRenderer implements AudioRenderer {
}
@Override
public int setup(int audioConfiguration, int sampleRate, int samplesPerFrame) {
public int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame) {
int channelConfig;
int bytesPerFrame;
switch (audioConfiguration)
switch (audioConfiguration.channelCount)
{
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
bytesPerFrame = 2 * samplesPerFrame * 2;
break;
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
bytesPerFrame = 6 * samplesPerFrame * 2;
break;
case 8:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// AudioFormat.CHANNEL_OUT_7POINT1_SURROUND isn't available until Android 6.0,
// yet the CHANNEL_OUT_SIDE_LEFT and CHANNEL_OUT_SIDE_RIGHT constants were added
// in 5.0, so just hardcode the constant so we can work on Lollipop.
channelConfig = 0x000018fc; // AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
}
else {
// On KitKat and lower, creation of the AudioTrack will fail if we specify
// CHANNEL_OUT_SIDE_LEFT or CHANNEL_OUT_SIDE_RIGHT. That leaves us with
// the old CHANNEL_OUT_7POINT1 which uses left-of-center and right-of-center
// speakers instead of side-left and side-right. This non-standard layout
// is probably not what the user wants, but we don't really have a choice.
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1;
}
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return -1;
}
LimeLog.info("Audio channel config: "+String.format("0x%X", channelConfig));
bytesPerFrame = audioConfiguration.channelCount * samplesPerFrame * 2;
// We're not supposed to request less than the minimum
// buffer size for our buffer, but it appears that we can
// do this on many devices and it lowers audio latency.
@@ -1,5 +1,6 @@
package com.limelight.binding.input;
import android.app.Activity;
import android.content.Context;
import android.hardware.input.InputManager;
import android.hardware.usb.UsbDevice;
@@ -51,7 +52,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
private final NvConnection conn;
private final Context activityContext;
private final Activity activityContext;
private final double stickDeadzone;
private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
@@ -61,7 +62,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final PreferenceConfiguration prefConfig;
private short currentControllers, initialControllers;
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
public ControllerHandler(Activity activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
this.activityContext = activityContext;
this.conn = conn;
this.gestures = gestures;
@@ -108,6 +109,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
defaultContext.assignedControllerNumber = true;
defaultContext.external = false;
// Some devices (GPD XD) have a back button which sends input events
// with device ID == 0. This hits the default context which would normally
@@ -146,6 +148,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (context != null) {
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
releaseControllerNumber(context);
context.destroy();
inputDeviceContexts.remove(deviceId);
}
}
@@ -160,10 +163,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public void stop() {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
deviceContext.destroy();
}
if (deviceContext.vibrator != null) {
deviceContext.vibrator.cancel();
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
deviceContext.destroy();
}
deviceVibrator.cancel();
@@ -264,9 +269,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
InputDeviceContext devContext = (InputDeviceContext) context;
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
if (devContext.name != null &&
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
if (!devContext.external) {
LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0;
}
@@ -327,6 +330,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.id = device.getControllerId();
context.device = device;
context.external = true;
context.vendorId = device.getVendorId();
context.productId = device.getProductId();
context.leftStickDeadzoneRadius = (float) stickDeadzone;
context.rightStickDeadzoneRadius = (float) stickDeadzone;
@@ -343,6 +350,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return true;
}
String deviceName = dev.getName();
if (deviceName.contains("gpio") || // This is the back button on Shield portable consoles
deviceName.contains("joy_key") || // These are the gamepad buttons on the Archos Gamepad 2
deviceName.contains("keypad") || // These are gamepad buttons on the XPERIA Play
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.01") || // Gamepad on Shield Portable
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.02")) // Gamepad on Shield Portable (?)
{
LimeLog.info(dev.getName()+" is internal by hardcoded mapping");
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
return dev.isExternal();
@@ -376,9 +394,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
// Classify this device as a remote by name if it has no joystick axes
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) == null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) == null &&
devName.toLowerCase().contains("remote")) {
if (!hasJoystickAxes(dev) && devName.toLowerCase().contains("remote")) {
return true;
}
@@ -401,11 +417,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// Note that we are explicitly NOT excluding the current device we're examining here,
// since the other gamepad buttons may be on our current device and that's fine.
boolean[] keys = currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_A);
if (keys[0]) {
if (currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT)[0]) {
foundInternalSelect = true;
}
if (keys[1]) {
// We don't check KEYCODE_BUTTON_A here, since the Shield Android TV has a
// virtual mouse device that claims to have KEYCODE_BUTTON_A. Instead, we rely
// on the SOURCE_GAMEPAD flag to be set on gamepad devices.
if (hasGamepadButtons(currentDev)) {
foundInternalGamepad = true;
}
}
@@ -417,8 +436,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
return !foundInternalGamepad || foundInternalSelect;
}
return false;
else {
// For external devices, we want to pass through the back button if the device
// has no gamepad axes or gamepad buttons.
return !hasJoystickAxes(dev) && !hasGamepadButtons(dev);
}
}
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
@@ -426,7 +448,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
String devName = dev.getName();
LimeLog.info("Creating controller context for device: "+devName);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
LimeLog.info("Vendor ID: "+dev.getVendorId());
LimeLog.info("Product ID: "+dev.getProductId());
}
@@ -434,6 +456,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.name = devName;
context.id = dev.getId();
context.external = isExternal(dev);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
context.vendorId = dev.getVendorId();
context.productId = dev.getProductId();
}
if (dev.getVibrator().hasVibrator()) {
context.vibrator = dev.getVibrator();
@@ -476,7 +504,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
if (rxRange != null && ryRange != null && devName != null) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (dev.getVendorId() == 0x054c) { // Sony
if (dev.hasKeys(KeyEvent.KEYCODE_BUTTON_C)[0]) {
LimeLog.info("Detected non-standard DualShock 4 mapping");
@@ -571,7 +599,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// The device name provided is just "Gamepad" which is pretty useless, so we
// use VID/PID instead
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
@@ -590,7 +618,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (devName.contains("ASUS Gamepad")) {
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
// is Android 5.0 anyway
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) {
context.backIsStart = true;
@@ -799,6 +827,43 @@ 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;
}
if ((context.vendorId == 0x057e && context.productId == 0x2009) || // Switch Pro controller
(context.vendorId == 0x0f0d && context.productId == 0x00c1)) { // HORIPAD for Switch
switch (event.getScanCode()) {
case 0x130:
return KeyEvent.KEYCODE_BUTTON_A;
case 0x131:
return KeyEvent.KEYCODE_BUTTON_B;
case 0x132:
return KeyEvent.KEYCODE_BUTTON_X;
case 0x133:
return KeyEvent.KEYCODE_BUTTON_Y;
case 0x134:
return KeyEvent.KEYCODE_BUTTON_L1;
case 0x135:
return KeyEvent.KEYCODE_BUTTON_R1;
case 0x136:
return KeyEvent.KEYCODE_BUTTON_L2;
case 0x137:
return KeyEvent.KEYCODE_BUTTON_R2;
case 0x138:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 0x139:
return KeyEvent.KEYCODE_BUTTON_START;
case 0x13A:
return KeyEvent.KEYCODE_BUTTON_THUMBL;
case 0x13B:
return KeyEvent.KEYCODE_BUTTON_THUMBR;
case 0x13D:
return KeyEvent.KEYCODE_BUTTON_MODE;
}
}
if (context.usesLinuxGamepadStandardFaceButtons) {
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
switch (event.getScanCode()) {
@@ -887,19 +952,25 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return KeyEvent.KEYCODE_BUTTON_MODE;
}
}
else if (context.vendorId == 0x0b05 && // ASUS
(context.productId == 0x7900 || // Kunai - USB
context.productId == 0x7902)) // Kunai - Bluetooth
{
// ROG Kunai has special M1-M4 buttons that are accessible via the
// joycon-style detachable controllers that we should map to Start
// and Select.
switch (event.getScanCode()) {
case 264:
case 266:
return KeyEvent.KEYCODE_BUTTON_START;
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
return 0;
case 265:
case 267:
return KeyEvent.KEYCODE_BUTTON_SELECT;
}
}
else if (context.hatXAxis == -1 &&
if (context.hatXAxis == -1 &&
context.hatYAxis == -1 &&
/* FIXME: There's no good way to know for sure if xpad is bound
to this device, so we won't use the name to validate if these
@@ -957,6 +1028,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return keyCode;
}
private int handleFlipFaceButtons(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_A:
return KeyEvent.KEYCODE_BUTTON_B;
case KeyEvent.KEYCODE_BUTTON_B:
return KeyEvent.KEYCODE_BUTTON_A;
case KeyEvent.KEYCODE_BUTTON_X:
return KeyEvent.KEYCODE_BUTTON_Y;
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_X;
default:
return keyCode;
}
}
private Vector2d populateCachedVector(float x, float y) {
// Reinitialize our cached Vector2d object
inputVector.initialize(x, y);
@@ -1030,17 +1116,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) {
context.inputMap |= ControllerPacket.LEFT_FLAG;
context.hatXAxisUsed = true;
}
else if (hatX > 0.5) {
context.inputMap |= ControllerPacket.RIGHT_FLAG;
context.hatXAxisUsed = true;
}
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) {
context.inputMap |= ControllerPacket.UP_FLAG;
context.hatYAxisUsed = true;
}
else if (hatY > 0.5) {
context.inputMap |= ControllerPacket.DOWN_FLAG;
context.hatYAxisUsed = true;
}
}
@@ -1218,6 +1308,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
int keyCode = handleRemapping(context, event);
if (prefConfig.flipFaceButtons) {
keyCode = handleFlipFaceButtons(keyCode);
}
if (keyCode == 0) {
return true;
}
@@ -1255,15 +1350,31 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap &= ~ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
@@ -1347,6 +1458,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
sendControllerInputPacket(context);
if (context.pendingExit && context.inputMap == 0) {
// All buttons from the quit combo are lifted. Finish the activity now.
activityContext.finish();
}
return true;
}
@@ -1357,6 +1474,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
int keyCode = handleRemapping(context, event);
if (prefConfig.flipFaceButtons) {
keyCode = handleFlipFaceButtons(keyCode);
}
if (keyCode == 0) {
return true;
}
@@ -1377,15 +1499,31 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap |= ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
@@ -1431,9 +1569,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return false;
}
// Start+Back+LB+RB is the quit combo
if (context.inputMap == (ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG |
ControllerPacket.LB_FLAG | ControllerPacket.RB_FLAG)) {
// Wait for the combo to lift and then finish the activity
context.pendingExit = true;
}
// Start+LB acts like select for controllers with one button
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
@@ -1443,10 +1588,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
// We detect select+start or start+RB as the special button combo
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG) ||
context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
@@ -1454,7 +1599,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
// We don't need to send repeat key down events, but the platform
// sends us events that claim to be repeats but they're from different
// devices, so we just send them all and deal with some duplicates.
@@ -1525,6 +1669,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (context != null) {
LimeLog.info("Removed controller: "+controller.getControllerId());
releaseControllerNumber(context);
context.destroy();
usbDeviceContexts.remove(controller.getControllerId());
}
}
@@ -1537,6 +1682,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
class GenericControllerContext {
public int id;
public boolean external;
public int vendorId;
public int productId;
public float leftStickDeadzoneRadius;
public float rightStickDeadzoneRadius;
@@ -1557,6 +1706,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public boolean mouseEmulationActive;
public Timer mouseEmulationTimer;
public short mouseEmulationLastInputMap;
public void destroy() {
if (mouseEmulationTimer != null) {
mouseEmulationTimer.cancel();
mouseEmulationTimer = null;
}
}
}
class InputDeviceContext extends GenericControllerContext {
@@ -1576,6 +1732,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public int hatXAxis = -1;
public int hatYAxis = -1;
public boolean hatXAxisUsed, hatYAxisUsed;
public boolean isNonStandardDualShock4;
public boolean usesLinuxGamepadStandardFaceButtons;
@@ -1585,6 +1742,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public boolean modeIsSelect;
public boolean ignoreBack;
public boolean hasJoystickAxes;
public boolean pendingExit;
public int emulatingButtonFlags = 0;
@@ -1597,9 +1755,25 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public long lastRbUpTime = 0;
public long startDownTime = 0;
@Override
public void destroy() {
super.destroy();
if (vibrator != null) {
vibrator.cancel();
}
}
}
class UsbDeviceContext extends GenericControllerContext {
public AbstractController device;
@Override
public void destroy() {
super.destroy();
// Nothing for now
}
}
}
@@ -1,17 +1,22 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.app.Activity;
import android.os.Build;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
@TargetApi(Build.VERSION_CODES.O)
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
// We extend AndroidPointerIconCaptureProvider because we want to also get the
// pointer icon hiding behavior over our stream view just in case pointer capture
// is unavailable on this system (ex: DeX, ChromeOS)
@TargetApi(Build.VERSION_CODES.O)
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider {
private View targetView;
public AndroidNativePointerCaptureProvider(View targetView) {
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
super(activity, targetView);
this.targetView = targetView;
}
@@ -31,11 +36,6 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
targetView.releasePointerCapture();
}
@Override
public boolean isCapturingActive() {
return targetView.hasPointerCapture();
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
@@ -7,55 +7,30 @@ import android.os.Build;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewGroup;
@TargetApi(Build.VERSION_CODES.N)
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
private ViewGroup rootViewGroup;
private View targetView;
private Context context;
public AndroidPointerIconCaptureProvider(Activity activity) {
public AndroidPointerIconCaptureProvider(Activity activity, View targetView) {
this.context = activity;
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
this.targetView = targetView;
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
private void setPointerIconOnAllViews(PointerIcon icon) {
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
View view = rootViewGroup.getChildAt(i);
view.setPointerIcon(icon);
}
rootViewGroup.setPointerIcon(icon);
}
@Override
public void enableCapture() {
super.enableCapture();
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
}
@Override
public void disableCapture() {
super.disableCapture();
setPointerIconOnAllViews(null);
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
targetView.setPointerIcon(null);
}
}
@@ -12,7 +12,7 @@ public class InputCaptureManager {
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android O+ native mouse capture");
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
return new AndroidNativePointerCaptureProvider(activity, activity.findViewById(R.id.surfaceView));
}
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
@@ -28,7 +28,7 @@ public class InputCaptureManager {
// Android N's native capture can't capture over system UI elements
// so we want to only use it if there's no other option.
LimeLog.info("Using Android N+ pointer hiding");
return new AndroidPointerIconCaptureProvider(activity);
return new AndroidPointerIconCaptureProvider(activity, activity.findViewById(R.id.surfaceView));
}
else {
LimeLog.info("Mouse capture not available");
@@ -75,8 +75,10 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
// All mouse events should use relative axes, even if they are zero. This avoids triggering
// cursor jumps if we get an event with no associated motion, like ACTION_DOWN or ACTION_UP.
return event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
}
@Override
@@ -3,6 +3,8 @@ package com.limelight.binding.input.driver;
public abstract class AbstractController {
private final int deviceId;
private final int vendorId;
private final int productId;
private UsbDriverListener listener;
@@ -15,6 +17,14 @@ public abstract class AbstractController {
return deviceId;
}
public int getVendorId() {
return vendorId;
}
public int getProductId() {
return productId;
}
protected void setButtonFlag(int buttonFlag, int data) {
if (data != 0) {
buttonFlags |= buttonFlag;
@@ -32,9 +42,11 @@ public abstract class AbstractController {
public abstract boolean start();
public abstract void stop();
public AbstractController(int deviceId, UsbDriverListener listener) {
public AbstractController(int deviceId, UsbDriverListener listener, int vendorId, int productId) {
this.deviceId = deviceId;
this.listener = listener;
this.vendorId = vendorId;
this.productId = productId;
}
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
@@ -22,7 +22,7 @@ public abstract class AbstractXboxController extends AbstractController {
protected UsbEndpoint inEndpt, outEndpt;
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(deviceId, listener);
super(deviceId, listener, device.getVendorId(), device.getProductId());
this.device = device;
this.connection = connection;
}
@@ -25,6 +25,7 @@ public class XboxOneController extends AbstractXboxController {
};
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
private static final byte[] ONE_S_INIT = {0x05, 0x20, 0x00, 0x0f, 0x06};
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
0x00, 0x00, 0x00, (byte)0x80, 0x00};
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
@@ -38,6 +39,8 @@ public class XboxOneController extends AbstractXboxController {
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
new InitPacket(0x0000, 0x0000, FW2015_INIT),
new InitPacket(0x045e, 0x02ea, ONE_S_INIT),
new InitPacket(0x045e, 0x0b00, ONE_S_INIT),
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
@@ -0,0 +1,272 @@
package com.limelight.binding.input.touch;
import android.os.SystemClock;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class AbsoluteTouchContext implements TouchContext {
private int lastTouchDownX = 0;
private int lastTouchDownY = 0;
private long lastTouchDownTime = 0;
private int lastTouchUpX = 0;
private int lastTouchUpY = 0;
private long lastTouchUpTime = 0;
private int lastTouchLocationX = 0;
private int lastTouchLocationY = 0;
private boolean cancelled;
private boolean confirmedLongPress;
private boolean confirmedTap;
private Timer longPressTimer;
private Timer tapDownTimer;
private float accumulatedScrollDelta;
private final NvConnection conn;
private final int actionIndex;
private final View targetView;
private static final int SCROLL_SPEED_DIVISOR = 20;
private static final int LONG_PRESS_TIME_THRESHOLD = 650;
private static final int LONG_PRESS_DISTANCE_THRESHOLD = 30;
private static final int DOUBLE_TAP_TIME_THRESHOLD = 250;
private static final int DOUBLE_TAP_DISTANCE_THRESHOLD = 60;
private static final int TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD = 100;
private static final int TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD = 20;
public AbsoluteTouchContext(NvConnection conn, int actionIndex, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.targetView = view;
}
@Override
public int getActionIndex()
{
return actionIndex;
}
@Override
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
{
if (!isNewFinger) {
// We don't handle finger transitions for absolute mode
return true;
}
lastTouchLocationX = lastTouchDownX = eventX;
lastTouchLocationY = lastTouchDownY = eventY;
lastTouchDownTime = SystemClock.uptimeMillis();
cancelled = confirmedTap = confirmedLongPress = false;
accumulatedScrollDelta = 0;
if (actionIndex == 0) {
// Start the timers
startTapDownTimer();
startLongPressTimer();
}
return true;
}
private boolean distanceExceeds(int deltaX, int deltaY, double limit) {
return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) > limit;
}
private void updatePosition(int eventX, int eventY) {
// We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT.
// Normalize these to the view size. We can't just drop them because we won't always get an event
// right at the boundary of the view, so dropping them would result in our cursor never really
// reaching the sides of the screen.
eventX = Math.min(Math.max(eventX, 0), targetView.getWidth());
eventY = Math.min(Math.max(eventY, 0), targetView.getHeight());
conn.sendMousePosition((short)eventX, (short)eventY, (short)targetView.getWidth(), (short)targetView.getHeight());
}
@Override
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
return;
}
if (actionIndex == 0) {
// Cancel the timers
cancelLongPressTimer();
cancelTapDownTimer();
// Raise the mouse buttons that we currently have down
if (confirmedLongPress) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
else if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
else {
// If we get here, this means that the tap completed within the touch down
// deadzone time. We'll need to send the touch down and up events now at the
// original touch down position.
tapConfirmed();
try {
// FIXME: Sleeping on the main thread sucks
Thread.sleep(50);
} catch (InterruptedException ignored) {}
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
lastTouchLocationX = lastTouchUpX = eventX;
lastTouchLocationY = lastTouchUpY = eventY;
lastTouchUpTime = SystemClock.uptimeMillis();
}
private synchronized void startLongPressTimer() {
longPressTimer = new Timer(true);
longPressTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (longPressTimer == null) {
return;
}
// Uncancellable now
longPressTimer = null;
// This timer should have already expired, but cancel it just in case
cancelTapDownTimer();
// Switch from a left click to a right click after a long press
confirmedLongPress = true;
if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
}
}, LONG_PRESS_TIME_THRESHOLD);
}
private synchronized void cancelLongPressTimer() {
if (longPressTimer != null) {
longPressTimer.cancel();
longPressTimer = null;
}
}
private synchronized void startTapDownTimer() {
tapDownTimer = new Timer(true);
tapDownTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (tapDownTimer == null) {
return;
}
// Uncancellable now
tapDownTimer = null;
// Start our tap
tapConfirmed();
}
}
}, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
}
private synchronized void cancelTapDownTimer() {
if (tapDownTimer != null) {
tapDownTimer.cancel();
tapDownTimer = null;
}
}
private void tapConfirmed() {
if (confirmedTap || confirmedLongPress) {
return;
}
confirmedTap = true;
cancelTapDownTimer();
// Left button down at original position
if (lastTouchDownTime - lastTouchUpTime > DOUBLE_TAP_TIME_THRESHOLD ||
distanceExceeds(lastTouchDownX - lastTouchUpX, lastTouchDownY - lastTouchUpY, DOUBLE_TAP_DISTANCE_THRESHOLD)) {
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
updatePosition(lastTouchDownX, lastTouchDownY);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
@Override
public boolean touchMoveEvent(int eventX, int eventY)
{
if (cancelled) {
return true;
}
if (actionIndex == 0) {
if (distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, LONG_PRESS_DISTANCE_THRESHOLD)) {
// Moved too far since touch down. Cancel the long press timer.
cancelLongPressTimer();
}
// Ignore motion within the deadzone period after touch down
if (confirmedTap || distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD)) {
tapConfirmed();
updatePosition(eventX, eventY);
}
}
else if (actionIndex == 1) {
accumulatedScrollDelta += (eventY - lastTouchLocationY) / (float)SCROLL_SPEED_DIVISOR;
if ((short)accumulatedScrollDelta != 0) {
conn.sendMouseHighResScroll((short)accumulatedScrollDelta);
accumulatedScrollDelta -= (short)accumulatedScrollDelta;
}
}
lastTouchLocationX = eventX;
lastTouchLocationY = eventY;
return true;
}
@Override
public void cancelTouch() {
cancelled = true;
// Cancel the timers
cancelLongPressTimer();
cancelTapDownTimer();
// Raise the mouse buttons
if (confirmedLongPress) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
else if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setPointerCount(int pointerCount) {
if (actionIndex == 0 && pointerCount > 1) {
cancelTouch();
}
}
}
@@ -1,5 +1,6 @@
package com.limelight.binding.input;
package com.limelight.binding.input.touch;
import android.os.SystemClock;
import android.view.View;
import com.limelight.nvstream.NvConnection;
@@ -8,7 +9,7 @@ import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class TouchContext {
public class RelativeTouchContext implements TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
@@ -32,8 +33,8 @@ public class TouchContext {
private static final int TAP_TIME_THRESHOLD = 250;
private static final int DRAG_TIME_THRESHOLD = 650;
public TouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight, View view)
public RelativeTouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
@@ -42,6 +43,7 @@ public class TouchContext {
this.targetView = view;
}
@Override
public int getActionIndex()
{
return actionIndex;
@@ -57,7 +59,7 @@ public class TouchContext {
private boolean isTap()
{
long timeDelta = System.currentTimeMillis() - originalTouchTime;
long timeDelta = SystemClock.uptimeMillis() - originalTouchTime;
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
}
@@ -72,7 +74,8 @@ public class TouchContext {
}
}
public boolean touchDownEvent(int eventX, int eventY)
@Override
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
{
// Get the view dimensions to scale inputs on this touch
xFactor = referenceWidth / (double)targetView.getWidth();
@@ -80,7 +83,7 @@ public class TouchContext {
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
originalTouchTime = SystemClock.uptimeMillis();
cancelled = confirmedDrag = confirmedMove = false;
distanceMoved = 0;
@@ -92,6 +95,7 @@ public class TouchContext {
return true;
}
@Override
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
@@ -128,7 +132,7 @@ public class TouchContext {
dragTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (TouchContext.this) {
synchronized (RelativeTouchContext.this) {
// Check if someone already set move
if (confirmedMove) {
return;
@@ -179,6 +183,7 @@ public class TouchContext {
}
}
@Override
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
@@ -223,6 +228,7 @@ public class TouchContext {
return true;
}
@Override
public void cancelTouch() {
cancelled = true;
@@ -235,7 +241,11 @@ public class TouchContext {
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setPointerCount(int pointerCount) {}
}
@@ -0,0 +1,11 @@
package com.limelight.binding.input.touch;
public interface TouchContext {
int getActionIndex();
void setPointerCount(int pointerCount);
boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger);
boolean touchMoveEvent(int eventX, int eventY);
void touchUpEvent(int eventX, int eventY);
void cancelTouch();
boolean isCancelled();
}
@@ -125,6 +125,13 @@ public class VirtualController {
elements.clear();
}
public void setOpacity(int opacity) {
for (VirtualControllerElement element : elements) {
element.setOpacity(opacity);
}
}
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
elements.add(element);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
@@ -205,7 +205,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
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_Y + 2 * BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -214,7 +215,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
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_Y + BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -223,7 +225,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
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_Y + BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -232,7 +235,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
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_Y, height),
screenScale(BUTTON_SIZE, height),
@@ -240,7 +244,7 @@ public class VirtualControllerConfigurationLoader {
);
controller.addElement(createLeftTrigger(
0, "LT", -1, controller, context),
1, "LT", -1, controller, context),
screenScale(TRIGGER_L_BASE_X, height),
screenScale(TRIGGER_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
@@ -248,7 +252,7 @@ public class VirtualControllerConfigurationLoader {
);
controller.addElement(createRightTrigger(
0, "RT", -1, controller, context),
1, "RT", -1, controller, context),
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
screenScale(TRIGGER_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
@@ -324,6 +328,8 @@ public class VirtualControllerConfigurationLoader {
screenScale(TRIGGER_HEIGHT, height)
);
}
controller.setOpacity(config.oscOpacity);
}
public static void saveProfile(final VirtualController controller,
@@ -361,4 +367,4 @@ public class VirtualControllerConfigurationLoader {
}
}
}
}
}
@@ -295,6 +295,15 @@ public abstract class VirtualControllerElement extends View {
invalidate();
}
public void setOpacity(int opacity) {
int hexOpacity = opacity * 255 / 100;
this.normalColor = (hexOpacity << 24) | (normalColor & 0x00FFFFFF);
this.pressedColor = (hexOpacity << 24) | (pressedColor & 0x00FFFFFF);
invalidate();
}
protected final float getPercent(float value, float percent) {
return value / 100 * percent;
}
@@ -45,6 +45,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4;
private boolean adaptivePlayback, directSubmit;
private boolean lowLatency;
private boolean constrainedHighProfile;
private boolean refFrameInvalidationAvc, refFrameInvalidationHevc;
private boolean refFrameInvalidationActive;
@@ -60,6 +61,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private boolean legacyFrameDropRendering = false;
private PerfOverlayListener perfListener;
private MediaFormat inputFormat;
private MediaFormat outputFormat;
private MediaFormat configuredFormat;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
@@ -161,7 +166,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// shared between AVC and HEVC decoders on the same device.
if (avcDecoder != null) {
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName());
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder);
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
@@ -264,6 +268,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
refFrameInvalidationActive = refFrameInvalidationAvc;
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(avcDecoder, mimeType);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder, mimeType);
}
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
mimeType = "video/hevc";
@@ -275,6 +282,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
refFrameInvalidationActive = refFrameInvalidationHevc;
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(hevcDecoder, mimeType);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(hevcDecoder, mimeType);
}
else {
// Unknown format
@@ -293,6 +303,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
// Avoid setting KEY_FRAME_RATE on Lollipop and earlier to reduce compatibility risk
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// We use prefs.fps instead of redrawRate here because the low latency hack in Game.java
// may leave us with an odd redrawRate value like 59 or 49 which might cause the decoder
// to puke. To be safe, we'll use the unmodified value.
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, prefs.fps);
}
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
// so we don't fill these pre-KitKat
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
@@ -300,7 +318,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && lowLatency) {
videoFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Set the Qualcomm vendor low latency extension if the Android R option is unavailable
if (MediaCodecHelper.decoderSupportsQcomVendorLowLatency(selectedDecoderName)) {
// MediaCodec supports vendor-defined format keys using the "vendor.<extension name>.<parameter name>" syntax.
// These allow access to functionality that is not exposed through documented MediaFormat.KEY_* values.
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/common/inc/vidc_vendor_extensions.h;l=67
//
// Examples of Qualcomm's vendor extensions for Snapdragon 845:
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/vdec/src/omx_vdec_extensions.hpp
// https://cs.android.com/android/_/android/platform/hardware/qcom/sm8150/media/+/0621ceb1c1b19564999db8293574a0e12952ff6c
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
}
// Operate at maximum rate to lower latency as much as possible on
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
// but that will actually result in the decoder crashing if it can't satisfy
@@ -308,8 +341,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
}
configuredFormat = videoFormat;
LimeLog.info("Configuring with format: "+configuredFormat);
try {
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// This will contain the actual accepted input format attributes
inputFormat = videoDecoder.getInputFormat();
LimeLog.info("Input format: "+inputFormat);
}
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -334,6 +377,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
legacyInputBuffers = videoDecoder.getInputBuffers();
}
} catch (Exception e) {
e.printStackTrace();
return -5;
@@ -445,7 +489,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
outputFormat = videoDecoder.getOutputFormat();
LimeLog.info("New output format: " + outputFormat);
break;
default:
break;
@@ -1018,12 +1063,31 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
str += "AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper()+"\n";
str += "AVC supported width range: "+avcWidthRange+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
Range<Double> avcFpsRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
str += "AVC achievable FPS range: "+avcFpsRange+"\n";
} catch (IllegalArgumentException e) {
str += "AVC achievable FPS range: UNSUPPORTED!\n";
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
str += "HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper()+"\n";
str += "HEVC supported width range: "+hevcWidthRange+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
Range<Double> hevcFpsRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
str += "HEVC achievable FPS range: " + hevcFpsRange + "\n";
} catch (IllegalArgumentException e) {
str += "HEVC achievable FPS range: UNSUPPORTED!\n";
}
}
}
str += "Configured format: "+renderer.configuredFormat+"\n";
str += "Input format: "+renderer.inputFormat+"\n";
str += "Output format: "+renderer.outputFormat+"\n";
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
str += "GL Renderer: "+renderer.glRenderer+"\n";
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
@@ -1031,6 +1095,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n";
str += "Low latency mode: "+renderer.lowLatency+"\n";
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
@@ -38,6 +38,7 @@ public class MediaCodecHelper {
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 boolean isLowEndSnapdragon = false;
private static boolean initialized = false;
@@ -189,6 +190,13 @@ public class MediaCodecHelper {
}
}
static {
qualcommDecoderPrefixes = new LinkedList<>();
qualcommDecoderPrefixes.add("omx.qcom");
qualcommDecoderPrefixes.add("c2.qti");
}
private static boolean isPowerVR(String glRenderer) {
return glRenderer.toLowerCase().contains("powervr");
}
@@ -330,7 +338,23 @@ public class MediaCodecHelper {
return System.nanoTime() / 1000000L;
}
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
public static boolean decoderSupportsLowLatency(MediaCodecInfo decoderInfo, String mimeType) {
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;
}
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo, String mimeType) {
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
@@ -339,7 +363,7 @@ public class MediaCodecHelper {
}
try {
if (decoderInfo.getCapabilitiesForType("video/avc").
if (decoderInfo.getCapabilitiesForType(mimeType).
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
{
// This will make getCapabilities() return that adaptive playback is supported
@@ -348,12 +372,20 @@ public class MediaCodecHelper {
}
} catch (Exception e) {
// Tolerate buggy codecs
e.printStackTrace();
}
}
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) {
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
}
@@ -2,6 +2,7 @@ package com.limelight.grid;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -40,10 +41,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
}
else if (prefs.smallIconMode) {
if (prefs.smallIconMode) {
return R.layout.app_grid_item_small;
}
else {
@@ -113,30 +111,17 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}
@Override
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
public void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView, prgView);
return true;
}
loader.populateImageView(obj.app, imgView, txtView);
@Override
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
// Select the text view so it starts marquee mode
txtView.setSelected(true);
// Return false to use the app's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
if (obj.isRunning) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.ic_play);
return true;
overlayView.setVisibility(View.VISIBLE);
}
else {
overlayView.setVisibility(View.GONE);
}
// No overlay
return false;
}
}
@@ -10,7 +10,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.ArrayList;
@@ -55,9 +54,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
return i;
}
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
public abstract boolean populateTextView(TextView txtView, T obj);
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
public abstract void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, T obj);
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
@@ -70,22 +67,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
TextView txtView = convertView.findViewById(R.id.grid_text);
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
if (imgView != null) {
if (!populateImageView(imgView, prgView, itemList.get(i))) {
imgView.setImageBitmap(null);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
}
populateView(imgView, prgView, txtView, overlayView, itemList.get(i));
return convertView;
}
@@ -22,15 +22,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
}
else if (prefs.smallIconMode) {
return R.layout.pc_grid_item_small;
}
else {
return R.layout.pc_grid_item;
}
return R.layout.pc_grid_item;
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
@@ -57,7 +49,8 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
}
@Override
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
public void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, PcView.ComputerObject obj) {
imgView.setImageResource(R.drawable.ic_computer);
if (obj.details.state == ComputerDetails.State.ONLINE) {
imgView.setAlpha(1.0f);
}
@@ -72,12 +65,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
prgView.setVisibility(View.INVISIBLE);
}
imgView.setImageResource(R.drawable.ic_computer);
return true;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
txtView.setText(obj.details.name);
if (obj.details.state == ComputerDetails.State.ONLINE) {
txtView.setAlpha(1.0f);
}
@@ -85,16 +73,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
txtView.setAlpha(0.4f);
}
// Return false to use the computer's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.OFFLINE) {
overlayView.setImageResource(R.drawable.ic_pc_offline);
overlayView.setAlpha(0.4f);
return true;
overlayView.setVisibility(View.VISIBLE);
}
// We must check if the status is exactly online and unpaired
// to avoid colliding with the loading spinner when status is unknown
@@ -102,8 +84,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
overlayView.setImageResource(R.drawable.ic_lock);
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.os.AsyncTask;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
@@ -89,7 +92,7 @@ public class CachedAppAssetLoader {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
private ScaledBitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
@@ -110,7 +113,7 @@ public class CachedAppAssetLoader {
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
return bmp;
}
@@ -132,29 +135,29 @@ public class CachedAppAssetLoader {
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private class LoaderTask extends AsyncTask<LoaderTuple, Void, ScaledBitmap> {
private final WeakReference<ImageView> imageViewRef;
private final WeakReference<ProgressBar> progressViewRef;
private final WeakReference<TextView> textViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
public LoaderTask(ImageView imageView, TextView textView, boolean diskOnly) {
this.imageViewRef = new WeakReference<>(imageView);
this.progressViewRef = new WeakReference<>(prgView);
this.textViewRef = new WeakReference<>(textView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
protected ScaledBitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the views are gone
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
if (isCancelled() || imageViewRef.get() == null || textViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
@@ -183,45 +186,61 @@ public class CachedAppAssetLoader {
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
final ProgressBar prgView = progressViewRef.get();
final TextView textView = textViewRef.get();
if (getLoaderTask(imageView) == this) {
// Now display the progress bar since we have to hit the network
if (prgView != null) {
prgView.setVisibility(View.VISIBLE);
}
// Set off another loader task on the network executor. This time our AsyncDrawable
// 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);
imageView.setVisibility(View.VISIBLE);
imageView.setImageDrawable(asyncDrawable);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
imageView.setVisibility(View.VISIBLE);
textView.setVisibility(View.VISIBLE);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
protected void onPostExecute(final ScaledBitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
final ProgressBar prgView = progressViewRef.get();
final TextView textView = textViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
// Fade in the box art
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Show the text if it's a placeholder
textView.setVisibility(isBitmapPlaceholder(bitmap) ? View.VISIBLE : View.GONE);
// Hide the progress bar
if (prgView != null) {
prgView.setVisibility(View.INVISIBLE);
}
if (imageView.getVisibility() == View.VISIBLE) {
// Fade out the placeholder first
Animation fadeOutAnimation = AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadeout);
fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
// Show the view
imageView.setVisibility(View.VISIBLE);
@Override
public void onAnimationEnd(Animation animation) {
// Fade in the new box art
imageView.setImageBitmap(bitmap.bitmap);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
imageView.startAnimation(fadeOutAnimation);
}
else {
// View is invisible already, so just fade in the new art
imageView.setImageBitmap(bitmap.bitmap);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
imageView.setVisibility(View.VISIBLE);
}
}
}
}
}
@@ -299,7 +318,13 @@ public class CachedAppAssetLoader {
});
}
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
private boolean isBitmapPlaceholder(ScaledBitmap bitmap) {
return (bitmap == null) ||
(bitmap.originalWidth == 130 && bitmap.originalHeight == 180) || // GFE 2.0
(bitmap.originalWidth == 628 && bitmap.originalHeight == 888); // GFE 3.0
}
public boolean populateImageView(NvApp app, ImageView imgView, TextView textView) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
@@ -309,22 +334,26 @@ public class CachedAppAssetLoader {
return true;
}
// Hide the progress bar always on initial load
prgView.setVisibility(View.INVISIBLE);
// Always set the name text so we have it if needed later
textView.setText(app.getAppName());
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
ScaledBitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
imgView.setVisibility(View.VISIBLE);
imgView.setImageBitmap(bmp);
imgView.setImageBitmap(bmp.bitmap);
// Show the text if it's a placeholder bitmap
textView.setVisibility(isBitmapPlaceholder(bmp) ? View.VISIBLE : View.GONE);
return true;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(imgView, prgView, true);
final LoaderTask task = new LoaderTask(imgView, textView, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
textView.setVisibility(View.INVISIBLE);
imgView.setVisibility(View.INVISIBLE);
imgView.setImageDrawable(asyncDrawable);
@@ -64,7 +64,7 @@ public class DiskAssetLoader {
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());
// Don't bother with anything if it doesn't exist
@@ -110,27 +110,33 @@ public class DiskAssetLoader {
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bmp != null) {
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
return new ScaledBitmap(decodeOnlyOptions.outWidth, decodeOnlyOptions.outHeight, bmp);
}
}
else {
// On P, we can get a bitmap back in one step with ImageDecoder
final ScaledBitmap scaledBitmap = new ScaledBitmap();
try {
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
scaledBitmap.bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
@Override
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
scaledBitmap.originalWidth = imageInfo.getSize().getWidth();
scaledBitmap.originalHeight = imageInfo.getSize().getHeight();
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
if (isLowRamDevice) {
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
}
}
});
return scaledBitmap;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return bmp;
return null;
}
public File getFile(String computerUuid, int appId) {
@@ -1,37 +1,74 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.util.LruCache;
import com.limelight.LimeLog;
import java.lang.ref.SoftReference;
import java.util.HashMap;
public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
private static final LruCache<String, ScaledBitmap> memoryCache = new LruCache<String, ScaledBitmap>(maxMemory / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
protected int sizeOf(String key, ScaledBitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
return bitmap.bitmap.getByteCount() / 1024;
}
@Override
protected void entryRemoved(boolean evicted, String key, ScaledBitmap oldValue, ScaledBitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
if (evicted) {
// Keep a soft reference around to the bitmap as long as we can
evictionCache.put(key, new SoftReference<>(oldValue));
}
}
};
private static final HashMap<String, SoftReference<ScaledBitmap>> evictionCache = new HashMap<>();
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
final String key = constructKey(tuple);
ScaledBitmap bmp = memoryCache.get(key);
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
LimeLog.info("LRU cache hit for tuple: "+tuple);
return bmp;
}
return bmp;
SoftReference<ScaledBitmap> bmpRef = evictionCache.get(key);
if (bmpRef != null) {
bmp = bmpRef.get();
if (bmp != null) {
LimeLog.info("Eviction cache hit for tuple: "+tuple);
// Put this entry back into the LRU cache
evictionCache.remove(key);
memoryCache.put(key, bmp);
return bmp;
}
else {
// The data is gone, so remove the dangling SoftReference now
evictionCache.remove(key);
}
}
return null;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, ScaledBitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
// We must evict first because that will push all items into the eviction cache
memoryCache.evictAll();
evictionCache.clear();
}
}
@@ -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;
}
}
@@ -17,7 +17,6 @@ public class ConnectionContext {
public String serverGfeVersion;
public int negotiatedWidth, negotiatedHeight;
public int negotiatedFps;
public boolean negotiatedHdr;
public int videoCapabilities;
@@ -121,13 +121,11 @@ public class NvConnection {
// Lower resolution to 1080p
context.negotiatedWidth = 1920;
context.negotiatedHeight = 1080;
context.negotiatedFps = context.streamConfig.getRefreshRate();
}
else {
// Take what the client wanted
context.negotiatedWidth = context.streamConfig.getWidth();
context.negotiatedHeight = context.streamConfig.getHeight();
context.negotiatedFps = context.streamConfig.getRefreshRate();
}
//
@@ -236,6 +234,10 @@ public class NvConnection {
return;
}
context.connListener.stageComplete(appName);
} catch (GfeHttpResponseException e) {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, e.getErrorCode());
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
@@ -263,9 +265,9 @@ public class NvConnection {
int ret = MoonBridge.startConnection(context.serverAddress,
context.serverAppVersion, context.serverGfeVersion,
context.negotiatedWidth, context.negotiatedHeight,
context.negotiatedFps, context.streamConfig.getBitrate(),
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
context.streamConfig.getMaxPacketSize(),
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration().toInt(),
context.streamConfig.getHevcSupported(),
context.negotiatedHdr,
context.streamConfig.getHevcBitratePercentageMultiplier(),
@@ -289,6 +291,13 @@ public class NvConnection {
MoonBridge.sendMouseMove(deltaX, deltaY);
}
}
public void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight)
{
if (!isMonkey) {
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
}
}
public void sendMouseButtonDown(final byte mouseButton)
{
@@ -339,6 +348,12 @@ public class NvConnection {
}
}
public void sendMouseHighResScroll(final short scrollAmount) {
if (!isMonkey) {
MoonBridge.sendMouseHighResScroll(scrollAmount);
}
}
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
}
@@ -3,10 +3,10 @@ package com.limelight.nvstream;
public interface NvConnectionListener {
void stageStarting(String stage);
void stageComplete(String stage);
void stageFailed(String stage, long errorCode);
void stageFailed(String stage, int errorCode);
void connectionStarted();
void connectionTerminated(long errorCode);
void connectionTerminated(int errorCode);
void connectionStatusUpdate(int connectionStatus);
void displayMessage(String message);
@@ -9,16 +9,11 @@ public class StreamConfiguration {
public static final int STREAM_CFG_LOCAL = 0;
public static final int STREAM_CFG_REMOTE = 1;
public static final int STREAM_CFG_AUTO = 2;
private static final int CHANNEL_COUNT_STEREO = 2;
private static final int CHANNEL_COUNT_5_1 = 6;
private static final int CHANNEL_MASK_STEREO = 0x3;
private static final int CHANNEL_MASK_5_1 = 0xFC;
private NvApp app;
private int width, height;
private int refreshRate;
private int launchRefreshRate;
private int clientRefreshRateX100;
private int bitrate;
private boolean sops;
@@ -26,9 +21,7 @@ public class StreamConfiguration {
private boolean playLocalAudio;
private int maxPacketSize;
private int remote;
private int audioChannelMask;
private int audioChannelCount;
private int audioConfiguration;
private MoonBridge.AudioConfiguration audioConfiguration;
private boolean supportsHevc;
private int hevcBitratePercentageMultiplier;
private boolean enableHdr;
@@ -57,6 +50,11 @@ public class StreamConfiguration {
config.refreshRate = refreshRate;
return this;
}
public StreamConfiguration.Builder setLaunchRefreshRate(int refreshRate) {
config.launchRefreshRate = refreshRate;
return this;
}
public StreamConfiguration.Builder setBitrate(int bitrate) {
config.bitrate = bitrate;
@@ -113,21 +111,8 @@ public class StreamConfiguration {
return this;
}
public StreamConfiguration.Builder setAudioConfiguration(int audioConfig) {
if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) {
config.audioChannelCount = CHANNEL_COUNT_STEREO;
config.audioChannelMask = CHANNEL_MASK_STEREO;
}
else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) {
config.audioChannelCount = CHANNEL_COUNT_5_1;
config.audioChannelMask = CHANNEL_MASK_5_1;
}
else {
throw new IllegalArgumentException("Invalid audio configuration");
}
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
config.audioConfiguration = audioConfig;
return this;
}
@@ -147,13 +132,13 @@ public class StreamConfiguration {
this.width = 1280;
this.height = 720;
this.refreshRate = 60;
this.launchRefreshRate = 60;
this.bitrate = 10000;
this.maxPacketSize = 1024;
this.remote = STREAM_CFG_AUTO;
this.sops = true;
this.enableAdaptiveResolution = false;
this.audioChannelCount = CHANNEL_COUNT_STEREO;
this.audioChannelMask = CHANNEL_MASK_STEREO;
this.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
this.supportsHevc = false;
this.enableHdr = false;
this.attachedGamepadMask = 0;
@@ -170,6 +155,10 @@ public class StreamConfiguration {
public int getRefreshRate() {
return refreshRate;
}
public int getLaunchRefreshRate() {
return launchRefreshRate;
}
public int getBitrate() {
return bitrate;
@@ -198,16 +187,8 @@ public class StreamConfiguration {
public int getRemote() {
return remote;
}
public int getAudioChannelCount() {
return audioChannelCount;
}
public int getAudioChannelMask() {
return audioChannelMask;
}
public int getAudioConfiguration() {
public MoonBridge.AudioConfiguration getAudioConfiguration() {
return audioConfiguration;
}
@@ -1,7 +1,9 @@
package com.limelight.nvstream.av.audio;
import com.limelight.nvstream.jni.MoonBridge;
public interface AudioRenderer {
int setup(int audioConfiguration, int sampleRate, int samplesPerFrame);
int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame);
void start();
@@ -69,9 +69,9 @@ public class ComputerDetails {
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("Name: ").append(name).append("\n");
str.append("State: ").append(state).append("\n");
str.append("Active Address: ").append(activeAddress).append("\n");
str.append("Name: ").append(name).append("\n");
str.append("UUID: ").append(uuid).append("\n");
str.append("Local Address: ").append(localAddress).append("\n");
str.append("Remote Address: ").append(remoteAddress).append("\n");
@@ -58,4 +58,13 @@ public class NvApp {
public boolean isInitialized() {
return this.initialized;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("Name: ").append(appName).append("\n");
str.append("HDR: ").append(hdrSupported ? "Yes" : "No").append("\n");
str.append("ID: ").append(appId).append("\n");
return str.toString();
}
}
@@ -186,9 +186,20 @@ public class NvHTTP {
}
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
int statusCode = Integer.parseInt(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
// We use Long.parseLong() because in rare cases GFE can send back a status code of
// 0xFFFFFFFF, which will cause Integer.parseInt() to throw a NumberFormatException due
// to exceeding Integer.MAX_VALUE. We'll get the desired error code of -1 by just casting
// the resulting long into an int.
int statusCode = (int)Long.parseLong(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
if (statusCode != 200) {
throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message"));
String statusMsg = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message");
if (statusCode == -1 && "Invalid".equals(statusMsg)) {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
statusCode = 418;
statusMsg = "Missing audio capture device. Reinstall GeForce Experience.";
}
throw new GfeHttpResponseException(statusCode, statusMsg);
}
}
@@ -331,7 +342,7 @@ public class NvHTTP {
throw new FileNotFoundException(url);
}
else {
throw new IOException("HTTP request failed: "+response.code());
throw new GfeHttpResponseException(response.code(), response.message());
}
}
@@ -622,9 +633,10 @@ public class NvHTTP {
public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException {
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 60 when starting. This won't impact our ability
// to get > 60 FPS while actually streaming though.
int fps = context.negotiatedFps > 60 ? 60 : context.negotiatedFps;
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
int fps = context.streamConfig.getLaunchRefreshRate() > 60 ? 0 : context.streamConfig.getLaunchRefreshRate();
// Using an unsupported resolution (not 720p, 1080p, or 4K) causes
// GFE to force SOPS to 720p60. This is fine for < 720p resolutions like
@@ -648,7 +660,7 @@ public class NvHTTP {
"&rikeyid="+context.riKeyId +
(!enableHdr ? "" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0") +
"&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) +
"&surroundAudioInfo=" + ((context.streamConfig.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()) +
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo() +
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") +
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""),
false);
@@ -660,7 +672,7 @@ public class NvHTTP {
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/resume?" + buildUniqueIdUuidString() +
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
"&rikeyid="+context.riKeyId +
"&surroundAudioInfo=" + ((context.streamConfig.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()),
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo(),
false);
String resume = getXmlString(xmlStr, "resume");
return Integer.parseInt(resume) != 0;
@@ -7,8 +7,9 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public class MoonBridge {
/* See documentation in Limelight.h for information about these functions and constants */
public static final int AUDIO_CONFIGURATION_STEREO = 0;
public static final int AUDIO_CONFIGURATION_51_SURROUND = 1;
public static final AudioConfiguration AUDIO_CONFIGURATION_STEREO = new AudioConfiguration(2, 0x3);
public static final AudioConfiguration AUDIO_CONFIGURATION_51_SURROUND = new AudioConfiguration(6, 0x3F);
public static final AudioConfiguration AUDIO_CONFIGURATION_71_SURROUND = new AudioConfiguration(8, 0x63F);
public static final int VIDEO_FORMAT_H264 = 0x0001;
public static final int VIDEO_FORMAT_H265 = 0x0100;
@@ -32,6 +33,9 @@ public class MoonBridge {
public static final int CONN_STATUS_OKAY = 0;
public static final int CONN_STATUS_POOR = 1;
public static final int ML_ERROR_GRACEFUL_TERMINATION = 0;
public static final int ML_ERROR_NO_VIDEO_TRAFFIC = -100;
private static AudioRenderer audioRenderer;
private static VideoDecoderRenderer videoRenderer;
private static NvConnectionListener connectionListener;
@@ -45,6 +49,57 @@ public class MoonBridge {
return slices << 24;
}
public static class AudioConfiguration {
public final int channelCount;
public final int channelMask;
public AudioConfiguration(int channelCount, int channelMask) {
this.channelCount = channelCount;
this.channelMask = channelMask;
}
// Creates an AudioConfiguration from the integer value returned by moonlight-common-c
// See CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION() and CHANNEL_MASK_FROM_AUDIO_CONFIGURATION()
// in Limelight.h
private AudioConfiguration(int audioConfiguration) {
// Check the magic byte before decoding to make sure we got something that's actually
// a MAKE_AUDIO_CONFIGURATION()-based value and not something else like an older version
// hardcoded AUDIO_CONFIGURATION value from an earlier version of moonlight-common-c.
if ((audioConfiguration & 0xFF) != 0xCA) {
throw new IllegalArgumentException("Audio configuration has invalid magic byte!");
}
this.channelCount = (audioConfiguration >> 8) & 0xFF;
this.channelMask = (audioConfiguration >> 16) & 0xFFFF;
}
// See SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION() in Limelight.h
public int getSurroundAudioInfo() {
return channelMask << 16 | channelCount;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AudioConfiguration) {
AudioConfiguration that = (AudioConfiguration)obj;
return this.toInt() == that.toInt();
}
return false;
}
@Override
public int hashCode() {
return toInt();
}
// Returns the integer value expected by moonlight-common-c
// See MAKE_AUDIO_CONFIGURATION() in Limelight.h
public int toInt() {
return ((channelMask) << 16) | (channelCount << 8) | 0xCA;
}
}
public static int bridgeDrSetup(int videoFormat, int width, int height, int redrawRate) {
if (videoRenderer != null) {
return videoRenderer.setup(videoFormat, width, height, redrawRate);
@@ -86,7 +141,7 @@ public class MoonBridge {
public static int bridgeArInit(int audioConfiguration, int sampleRate, int samplesPerFrame) {
if (audioRenderer != null) {
return audioRenderer.setup(audioConfiguration, sampleRate, samplesPerFrame);
return audioRenderer.setup(new AudioConfiguration(audioConfiguration), sampleRate, samplesPerFrame);
}
else {
return -1;
@@ -129,7 +184,7 @@ public class MoonBridge {
}
}
public static void bridgeClStageFailed(int stage, long errorCode) {
public static void bridgeClStageFailed(int stage, int errorCode) {
if (connectionListener != null) {
connectionListener.stageFailed(getStageName(stage), errorCode);
}
@@ -141,7 +196,7 @@ public class MoonBridge {
}
}
public static void bridgeClConnectionTerminated(long errorCode) {
public static void bridgeClConnectionTerminated(int errorCode) {
if (connectionListener != null) {
connectionListener.connectionTerminated(errorCode);
}
@@ -187,6 +242,8 @@ public class MoonBridge {
public static native void sendMouseMove(short deltaX, short deltaY);
public static native void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight);
public static native void sendMouseButton(byte buttonEvent, byte mouseButton);
public static native void sendMultiControllerInput(short controllerNumber,
@@ -204,6 +261,8 @@ public class MoonBridge {
public static native void sendMouseScroll(byte scrollClicks);
public static native void sendMouseHighResScroll(short scrollAmount);
public static native String getStageName(int stage);
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
@@ -6,9 +6,11 @@ import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager;
import com.limelight.nvstream.jni.MoonBridge;
public class PreferenceConfiguration {
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
private static final String LEGACY_ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
static final String RESOLUTION_PREF_STRING = "list_resolution";
static final String FPS_PREF_STRING = "list_fps";
@@ -19,11 +21,11 @@ public class PreferenceConfiguration {
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String OSC_OPACITY_PREF_STRING = "seekbar_osc_opacity";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
static final String AUDIO_CONFIG_PREF_STRING = "list_audio_config";
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
@@ -38,18 +40,20 @@ public class PreferenceConfiguration {
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
private static final String FLIP_FACE_BUTTONS_PREF_STRING = "checkbox_flip_face_buttons";
private static final String TOUCHSCREEN_TRACKPAD_PREF_STRING = "checkbox_touchscreen_trackpad";
private static final String LATENCY_TOAST_PREF_STRING = "checkbox_enable_post_stream_toast";
static final String DEFAULT_RESOLUTION = "720p";
static final String DEFAULT_RESOLUTION = "1280x720";
static final String DEFAULT_FPS = "60";
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15;
private static final int DEFAULT_OPACITY = 90;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto";
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
@@ -64,83 +68,94 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_UNLOCK_FPS = false;
private static final boolean DEFAULT_VIBRATE_OSC = true;
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
private static final boolean DEFAULT_FLIP_FACE_BUTTONS = false;
private static final boolean DEFAULT_TOUCHSCREEN_TRACKPAD = true;
private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo
private static final boolean DEFAULT_LATENCY_TOAST = false;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
public static final int FORCE_H265_OFF = 1;
public static final String RES_360P = "640x360";
public static final String RES_480P = "854x480";
public static final String RES_720P = "1280x720";
public static final String RES_1080P = "1920x1080";
public static final String RES_1440P = "2560x1440";
public static final String RES_4K = "3840x2160";
public int width, height, fps;
public int bitrate;
public int videoFormat;
public int deadzonePercentage;
public int oscOpacity;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean smallIconMode, multiController, usbDriver, flipFaceButtons;
public boolean onscreenController;
public boolean onlyL3R3;
public boolean disableFrameDrop;
public boolean enableHdr;
public boolean enablePip;
public boolean enablePerfOverlay;
public boolean enableLatencyToast;
public boolean bindAllUsb;
public boolean mouseEmulation;
public boolean mouseNavButtons;
public boolean unlockFps;
public boolean vibrateOsc;
public boolean vibrateFallbackToDevice;
public boolean touchscreenTrackpad;
public MoonBridge.AudioConfiguration audioConfiguration;
private static int getHeightFromResolutionString(String resString) {
private static String convertFromLegacyResolutionString(String resString) {
if (resString.equalsIgnoreCase("360p")) {
return 360;
return RES_360P;
}
else if (resString.equalsIgnoreCase("480p")) {
return 480;
return RES_480P;
}
else if (resString.equalsIgnoreCase("720p")) {
return 720;
return RES_720P;
}
else if (resString.equalsIgnoreCase("1080p")) {
return 1080;
return RES_1080P;
}
else if (resString.equalsIgnoreCase("1440p")) {
return 1440;
return RES_1440P;
}
else if (resString.equalsIgnoreCase("4K")) {
return 2160;
return RES_4K;
}
else {
// Should be unreachable
return 720;
return RES_720P;
}
}
private static int getWidthFromResolutionString(String resString) {
int height = getHeightFromResolutionString(resString);
if (height == 480) {
// This isn't an exact 16:9 resolution
return 854;
}
else {
return (height * 16) / 9;
}
return Integer.parseInt(resString.split("x")[0]);
}
private static int getHeightFromResolutionString(String resString) {
return Integer.parseInt(resString.split("x")[1]);
}
private static String getResolutionString(int width, int height) {
switch (height) {
case 360:
return "360p";
return RES_360P;
case 480:
return "480p";
return RES_480P;
default:
case 720:
return "720p";
return RES_720P;
case 1080:
return "1080p";
return RES_1080P;
case 1440:
return "1440p";
return RES_1440P;
case 2160:
return "4K";
return RES_4K;
}
}
@@ -244,6 +259,15 @@ public class PreferenceConfiguration {
PreferenceConfiguration config = new PreferenceConfiguration();
// Migrate legacy preferences to the new locations
if (prefs.contains(LEGACY_ENABLE_51_SURROUND_PREF_STRING)) {
if (prefs.getBoolean(LEGACY_ENABLE_51_SURROUND_PREF_STRING, false)) {
prefs.edit()
.remove(LEGACY_ENABLE_51_SURROUND_PREF_STRING)
.putString(AUDIO_CONFIG_PREF_STRING, "51")
.apply();
}
}
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
if (str != null) {
if (str.equals("360p30")) {
@@ -302,21 +326,47 @@ public class PreferenceConfiguration {
else {
// Use the new preference location
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
// Convert legacy resolution strings to the new style
if (!resStr.contains("x")) {
resStr = PreferenceConfiguration.convertFromLegacyResolutionString(resStr);
prefs.edit().putString(RESOLUTION_PREF_STRING, resStr).apply();
}
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
}
if (!prefs.contains(SMALL_ICONS_PREF_STRING)) {
// We need to write small icon mode's default to disk for the settings page to display
// the current state of the option properly
prefs.edit().putBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)).apply();
}
// This must happen after the preferences migration to ensure the preferences are populated
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
if (config.bitrate == 0) {
config.bitrate = getDefaultBitrate(context);
}
String audioConfig = prefs.getString(AUDIO_CONFIG_PREF_STRING, DEFAULT_AUDIO_CONFIG);
if (audioConfig.equals("71")) {
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_71_SURROUND;
}
else if (audioConfig.equals("51")) {
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_51_SURROUND;
}
else /* if (audioConfig.equals("2")) */ {
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
}
config.videoFormat = getVideoFormatValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.oscOpacity = prefs.getInt(OSC_OPACITY_PREF_STRING, DEFAULT_OPACITY);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences
@@ -324,10 +374,8 @@ public class PreferenceConfiguration {
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
@@ -341,6 +389,9 @@ public class PreferenceConfiguration {
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PREF_STRING, DEFAULT_FLIP_FACE_BUTTONS);
config.touchscreenTrackpad = prefs.getBoolean(TOUCHSCREEN_TRACKPAD_PREF_STRING, DEFAULT_TOUCHSCREEN_TRACKPAD);
config.enableLatencyToast = prefs.getBoolean(LATENCY_TOAST_PREF_STRING, DEFAULT_LATENCY_TOAST);
return config;
}
@@ -5,6 +5,7 @@ import android.content.Context;
import android.os.Bundle;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
@@ -78,6 +79,8 @@ public class SeekBarPreference extends DialogPreference
valueText = new TextView(context);
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32);
// Default text for value; hides bug where OnSeekBarChangeListener isn't called when opacity is 0%
valueText.setText("0%");
params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
@@ -1,5 +1,6 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaCodecInfo;
@@ -7,6 +8,7 @@ import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
import android.os.Handler;
import android.os.Vibrator;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
@@ -54,9 +56,7 @@ public class StreamSettings extends Activity {
// Check for changes that require a UI reload to take effect
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
if (newPrefs.listMode != previousPrefs.listMode ||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
!newPrefs.language.equals(previousPrefs.language)) {
if (!newPrefs.language.equals(previousPrefs.language)) {
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -141,18 +141,40 @@ public class StreamSettings extends Activity {
// hide on-screen controls category on non touch screen devices
if (!getActivity().getPackageManager().
hasSystemFeature("android.hardware.touchscreen")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_onscreen_controls");
screen.removePreference(category);
{
PreferenceCategory category =
(PreferenceCategory) findPreference("category_onscreen_controls");
screen.removePreference(category);
}
{
PreferenceCategory category =
(PreferenceCategory) findPreference("category_input_settings");
category.removePreference(findPreference("checkbox_touchscreen_trackpad"));
}
}
// Remove PiP mode on devices pre-Oreo
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Remove PiP mode on devices pre-Oreo or where the feature is not available (some low RAM devices)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
(PreferenceCategory) findPreference("category_ui_settings");
category.removePreference(findPreference("checkbox_enable_pip"));
}
// Remove the vibration options if the device can't vibrate
if (!((Vibrator)getActivity().getSystemService(Context.VIBRATOR_SERVICE)).hasVibrator()) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_input_settings");
category.removePreference(findPreference("checkbox_vibrate_fallback"));
// The entire OSC category may have already been removed by the touchscreen check above
category = (PreferenceCategory) findPreference("category_onscreen_controls");
if (category != null) {
category.removePreference(findPreference("checkbox_vibrate_osc"));
}
}
int maxSupportedFps = 0;
// Hide non-supported resolution/FPS combinations
@@ -241,33 +263,33 @@ public class StreamSettings extends Activity {
if (maxSupportedResW != 0) {
if (maxSupportedResW < 3840) {
// 4K is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_4K, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P);
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedResW < 2560) {
// 1440p is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P);
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedResW < 1920) {
// 1080p is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_720P);
resetBitrateToDefault(prefs, null, null);
}
});
@@ -302,12 +324,24 @@ public class StreamSettings extends Activity {
// Never remove 30 FPS or 60 FPS
}
// Android L introduces proper 7.1 surround sound support. Remove the 7.1 option
// for earlier versions of Android to prevent AudioTrack initialization issues.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
LimeLog.info("Excluding 7.1 surround sound option based on OS");
removeValue(PreferenceConfiguration.AUDIO_CONFIG_PREF_STRING, "71", new Runnable() {
@Override
public void run() {
setValue(PreferenceConfiguration.AUDIO_CONFIG_PREF_STRING, "51");
}
});
}
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
// that the unlock FPS option relies on to not massively increase latency.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
LimeLog.info("Excluding unlock FPS toggle based on OS");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_unlock_fps"));
}
else {
@@ -3,41 +3,22 @@ package com.limelight.utils;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import com.limelight.HelpActivity;
public class HelpLauncher {
private static boolean isKnownBrowser(Context context, Intent i) {
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
if (resolvedActivity == null) {
// No browser
return false;
}
String name = resolvedActivity.activityInfo.name;
if (name == null) {
return false;
}
name = name.toLowerCase();
return name.contains("chrome") || name.contains("firefox");
}
private static void launchUrl(Context context, String url) {
// Try to launch the default browser
try {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
// Several Android TV devices will lie and say they do have a browser
// even though the OS just shows an error dialog if we try to use it. We need to
// be a bit more clever on these devices and detect if the browser is a legitimate
// browser or just a fake error message activity.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
isKnownBrowser(context, i)) {
// Several Android TV devices will lie and say they do have a browser even though the OS
// just shows an error dialog if we try to use it. We used to try to be clever and check
// the package name of the resolved intent, but it's not worth it anymore with Android 11's
// package visibility changes. We'll just always use the WebView on Android TV.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
context.startActivity(i);
return;
}
@@ -192,4 +192,13 @@ public class ShortcutHelper {
}
}
}
public void enableAppShortcut(ComputerDetails computer, NvApp app) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
String id = getShortcutIdForGame(computer, app);
if (getInfoForId(id) != null) {
sm.enableShortcuts(Collections.singletonList(id));
}
}
}
}
+39 -75
View File
@@ -87,9 +87,9 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
BridgeArPlaySampleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArPlaySample", "([S)V");
BridgeClStageStartingMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageStarting", "(I)V");
BridgeClStageCompleteMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageComplete", "(I)V");
BridgeClStageFailedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageFailed", "(IJ)V");
BridgeClStageFailedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageFailed", "(II)V");
BridgeClConnectionStartedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStarted", "()V");
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(J)V");
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(I)V");
BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V");
BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V");
}
@@ -98,12 +98,9 @@ int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void*
JNIEnv* env = GetThreadEnv();
int err;
if ((*env)->ExceptionCheck(env)) {
return -1;
}
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSetupMethod, videoFormat, width, height, redrawRate);
if ((*env)->ExceptionCheck(env)) {
// This is called on a Java thread, so it's safe to return
return -1;
}
else if (err != 0) {
@@ -119,20 +116,12 @@ int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void*
void BridgeDrStart(void) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStartMethod);
}
void BridgeDrStop(void) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStopMethod);
}
@@ -141,10 +130,6 @@ void BridgeDrCleanup(void) {
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrCleanupMethod);
}
@@ -152,10 +137,6 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
JNIEnv* env = GetThreadEnv();
int ret;
if ((*env)->ExceptionCheck(env)) {
return DR_OK;
}
// Increase the size of our frame data buffer if our frame won't fit
if ((*env)->GetArrayLength(env, DecodedFrameBuffer) < decodeUnit->fullLength) {
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
@@ -178,6 +159,8 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
decodeUnit->frameNumber, decodeUnit->receiveTimeMs);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
return DR_OK;
}
else if (ret != DR_OK) {
@@ -192,22 +175,27 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
currentEntry = currentEntry->next;
}
return (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
decodeUnit->frameNumber,
decodeUnit->receiveTimeMs);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
return DR_OK;
}
else {
return ret;
}
}
int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags) {
JNIEnv* env = GetThreadEnv();
int err;
if ((*env)->ExceptionCheck(env)) {
return -1;
}
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration, opusConfig->sampleRate, opusConfig->samplesPerFrame);
if ((*env)->ExceptionCheck(env)) {
// This is called on a Java thread, so it's safe to return
err = -1;
}
if (err == 0) {
@@ -233,20 +221,12 @@ int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusCon
void BridgeArStart(void) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStartMethod);
}
void BridgeArStop(void) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStopMethod);
}
@@ -257,21 +237,13 @@ void BridgeArCleanup() {
(*env)->DeleteGlobalRef(env, DecodedAudioBuffer);
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArCleanupMethod);
}
void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
jshort* decodedData = (*env)->GetShortArrayElements(env, DecodedAudioBuffer, 0);
jshort* decodedData = (*env)->GetPrimitiveArrayCritical(env, DecodedAudioBuffer, NULL);
int decodeLen = opus_multistream_decode(Decoder,
(const unsigned char*)sampleData,
@@ -280,85 +252,77 @@ void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
OpusConfig.samplesPerFrame,
0);
if (decodeLen > 0) {
// We must release the array elements first to ensure the data is copied before the callback
(*env)->ReleaseShortArrayElements(env, DecodedAudioBuffer, decodedData, 0);
// We must release the array elements before making further JNI calls
(*env)->ReleasePrimitiveArrayCritical(env, DecodedAudioBuffer, decodedData, 0);
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArPlaySampleMethod, DecodedAudioBuffer);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
}
}
else {
// We can abort here to avoid the copy back since no data was modified
(*env)->ReleaseShortArrayElements(env, DecodedAudioBuffer, decodedData, JNI_ABORT);
(*env)->ReleasePrimitiveArrayCritical(env, DecodedAudioBuffer, decodedData, JNI_ABORT);
}
}
void BridgeClStageStarting(int stage) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageStartingMethod, stage);
}
void BridgeClStageComplete(int stage) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageCompleteMethod, stage);
}
void BridgeClStageFailed(int stage, long errorCode) {
void BridgeClStageFailed(int stage, int errorCode) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageFailedMethod, stage, errorCode);
}
void BridgeClConnectionStarted(void) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStartedMethod, NULL);
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStartedMethod);
}
void BridgeClConnectionTerminated(long errorCode) {
void BridgeClConnectionTerminated(int errorCode) {
JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionTerminatedMethod, errorCode);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
}
}
void BridgeClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) {
JNIEnv* env = GetThreadEnv();
// The seemingly redundant short casts are required in order to convert the unsigned short to a signed short.
// If we leave it as an unsigned short, CheckJNI will fail when the value exceeds 32767. The cast itself is
// fine because the Java code treats the value as unsigned even though it's stored in a signed type.
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClRumbleMethod, controllerNumber, (short)lowFreqMotor, (short)highFreqMotor);
if ((*env)->ExceptionCheck(env)) {
return;
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClRumbleMethod, controllerNumber, lowFreqMotor, highFreqMotor);
}
void BridgeClConnectionStatusUpdate(int connectionStatus) {
JNIEnv* env = GetThreadEnv();
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStatusUpdateMethod, connectionStatus);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStatusUpdateMethod, connectionStatus);
}
void BridgeClLogMessage(const char* format, ...) {
@@ -10,6 +10,12 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendMouseMove(JNIEnv *env, jclass cla
LiSendMouseMoveEvent(deltaX, deltaY);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_sendMousePosition(JNIEnv *env, jclass clazz,
jshort x, jshort y, jshort referenceWidth, jshort referenceHeight) {
LiSendMousePositionEvent(x, y, referenceWidth, referenceHeight);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_sendMouseButton(JNIEnv *env, jclass clazz, jbyte buttonEvent, jbyte mouseButton) {
LiSendMouseButtonEvent(buttonEvent, mouseButton);
@@ -43,6 +49,11 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendMouseScroll(JNIEnv *env, jclass c
LiSendScrollEvent(scrollClicks);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_sendMouseHighResScroll(JNIEnv *env, jclass clazz, jshort scrollAmount) {
LiSendHighResScrollEvent(scrollAmount);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_stopConnection(JNIEnv *env, jclass clazz) {
LiStopConnection();
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:duration="100"/>
</set>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:duration="100"/>
</set>
+20 -40
View File
@@ -1,49 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="150dp"
android:layout_height="200dp"
android:padding="10dp">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="175dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="75dp"
android:layout_height="75dp"
android:indeterminate="true">
</ProgressBar>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
</RelativeLayout>
android:layout_marginTop="10dp"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:layout_height="match_parent"
android:layout_below="@id/grid_overlay"
android:layout_margin="5dp"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="18sp" >
android:textSize="18sp">
</TextView>
</RelativeLayout>
+20 -40
View File
@@ -1,49 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="100dp"
android:layout_height="133dp"
android:padding="5dp">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="117dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminate="true">
</ProgressBar>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
android:layout_marginTop="10dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:layout_height="match_parent"
android:layout_below="@id/grid_overlay"
android:layout_margin="5dp"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="14sp" >
android:textSize="14sp">
</TextView>
</RelativeLayout>
+1 -1
View File
@@ -4,7 +4,7 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:columnWidth="150dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
@@ -4,7 +4,7 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:columnWidth="100dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fastScrollEnabled="true"
android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false"
android:clipToPadding="false"/>
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="75dp"
android:layout_height="75dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:layout_width="33dp"
android:layout_height="33dp"
android:indeterminate="true">
</ProgressBar>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="75dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:clipToPadding="false"
android:gravity="center"/>
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:padding="10dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/grid_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textIsSelectable="false"
android:textSize="16sp" >
</TextView>
</LinearLayout>
+2 -4
View File
@@ -131,8 +131,6 @@
<string name="summary_checkbox_enable_pip">Stream auch während des Multitaskings anzeigen (ohne Steuerung)</string>
<string name="category_audio_settings">Audio Einstellungen</string>
<string name="title_checkbox_51_surround">5.1 Surround Sound aktivieren</string>
<string name="summary_checkbox_51_surround">Deaktivieren wenn die Audiowiedergabe fehlerhaft ist. Setzt GFE 2.7 oder neuer voraus.</string>
<string name="category_input_settings">Eingabe Einstellungen</string>
<string name="title_checkbox_multi_controller">Automatische GamePad-Erkennung</string>
@@ -166,8 +164,6 @@
<string name="category_ui_settings">UI Einstellungen</string>
<string name="title_language_list">Sprache</string>
<string name="summary_language_list">Sprache die Moonlight verwenden soll</string>
<string name="title_checkbox_list_mode">Zeige Listen anstelle von Rastern.</string>
<string name="summary_checkbox_list_mode">Zeige Apps und PCs als Liste anstelle eines Rasters</string>
<string name="title_checkbox_small_icon_mode">Verwende kleine Icons</string>
<string name="summary_checkbox_small_icon_mode">Verwende kleine Icons in der Rasteransicht, um mehr gleichzeitig anzeigen zu können</string>
@@ -186,5 +182,7 @@
<string name="summary_enable_hdr">HDR-Streaming sofern dies von der PC GPU unterstützt wird. HDR erfordert eine GPU der GTX 1000 Serie oder neuer.</string>
<string name="title_enable_perf_overlay">Performance Overlay aktivieren</string>
<string name="summary_enable_perf_overlay">Leistungsmerkmale während des Streamens in Echtzeit einblenden.</string>
<string name="suffix_osc_opacity">%</string>
<string name="dialog_title_osc_opacity">Transparenz</string>
</resources>
-4
View File
@@ -94,8 +94,6 @@
<string name="summary_checkbox_disable_warnings">Desactivar mensajes de advertencia en pantalla durante la transmisión</string>
<string name="category_audio_settings">Configuración de audio</string>
<string name="title_checkbox_51_surround">Activar sonido 5.1 surround</string>
<string name="summary_checkbox_51_surround">Desmarcar si experimentas problemas de audio. Requiere GFE 2.7 o superior.</string>
<string name="title_checkbox_multi_controller">Soporte para múltiples mandos</string>
<string name="summary_checkbox_multi_controller">Si no está marcado, todos los mandos aparecen como uno solo</string>
@@ -113,8 +111,6 @@
<string name="category_ui_settings">Configuración de la interfaz</string>
<string name="title_language_list">Idioma</string>
<string name="summary_language_list">Idioma a usar por Moonlight</string>
<string name="title_checkbox_list_mode">Usar listas en lugar de cuadrículas</string>
<string name="summary_checkbox_list_mode">Mostrar aplicaciones y PCs en listas en lugar de el cuadrículas</string>
<string name="title_checkbox_small_icon_mode">Usar iconos pequeños</string>
<string name="summary_checkbox_small_icon_mode">Usar iconos pequeños en las entradas de cuadrículas para permitir más entradas en la pantalla</string>
+11 -1
View File
@@ -1,11 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="fps_names">
<item>30 IPS</item>
<item>60 IPS</item>
<item>90 IPS</item>
<item>120 IPS</item>
</string-array>
<string-array name="audio_config_names">
<item>Stéréo</item>
<item>Son surround 5.1</item>
<item>Son surround 7.1</item>
</string-array>
<string-array name="decoder_names">
<item>Sélection automatique du décodeur</item>
<item>Contraindre le décodage logiciel</item>
<item>Contraindre le décodage matériel</item>
</string-array>
<string-array name="video_format_names">
<item>Utiliser H.265 uniquement s\'il est stable</item>
<item>Utilisez toujours H.265 (mais il peut planter)</item>
+6 -4
View File
@@ -132,8 +132,8 @@
<string name="summary_checkbox_enable_pip">Permet de visualiser le flux (sans le contrôleur) tout en multitâche</string>
<string name="category_audio_settings">Paramètres audio</string>
<string name="title_checkbox_51_surround">Activer son surround 5.1</string>
<string name="summary_checkbox_51_surround">Décochez si vous rencontrez des problèmes audio. Nécessite GFE 2.7 ou supérieur.</string>
<string name="title_audio_config_list">Configuration son surround</string>
<string name="summary_audio_config_list">Activer le son surround 5.1 ou 7.1 pour les systèmes home cinéma</string>
<string name="category_input_settings">Paramètres d\'entrée</string>
<string name="title_checkbox_multi_controller">Prise en charge de plusieurs contrôleurs</string>
@@ -163,12 +163,14 @@
<string name="dialog_title_reset_osc">Réinitialiser la mise en page</string>
<string name="dialog_text_reset_osc">Êtes-vous sûr de vouloir supprimer la disposition des commandes à l\'écran que vous avez sauvegardée?</string>
<string name="toast_reset_osc_success">Les contrôles à l\'écran sont réinitialisés</string>
<string name="title_osc_opacity">Modifier l\'opacité des contrôles à l\'écran</string>
<string name="summary_osc_opacity">Rendre les contrôles à l\'écran plus/moins transparents</string>
<string name="dialog_title_osc_opacity">Modifiez l\'opacité</string>
<string name="suffix_osc_opacity">%</string>
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
<string name="title_language_list">Langue</string>
<string name="summary_language_list">Langue à utiliser pour Moonlight</string>
<string name="title_checkbox_list_mode">Utiliser des listes au lieu des grilles</string>
<string name="summary_checkbox_list_mode">Afficher les applications et les PC en listes au lieu de grilles</string>
<string name="title_checkbox_small_icon_mode">Utiliser des petites icônes</string>
<string name="summary_checkbox_small_icon_mode">Utilisez les petites icônes dans les éléments de la grille pour permettre plus d\'éléments à l\'écran</string>
+2 -5
View File
@@ -113,8 +113,6 @@
<string name="summary_checkbox_enable_pip">Permette di osservare (ma non di controllare) la stream in multitasking</string>
<string name="category_audio_settings">Impostazioni audio</string>
<string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string>
<string name="summary_checkbox_51_surround">Se riscontri problemi, disabilitalo. Richiede GFE 2.7 o versioni sucessive.</string>
<string name="title_checkbox_multi_controller">Supporto a più controller</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string>
@@ -141,8 +139,6 @@
<string name="category_ui_settings">Impostazioni dell\'interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Moonlight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza le applicazioni e i PC in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella griglia per avere più oggetti a schermo</string>
@@ -159,5 +155,6 @@
<string name="summary_video_format">H.265 riduce i requisiti di larghezza di banda video ma richiede un dispositivo molto recente</string>
<string name="title_enable_hdr">Abilita HDR (sperimentale)</string>
<string name="summary_enable_hdr">Utilizza l\'HDR quando il gioco e la scheda video del PC lo supportano. L\'HDR richiede una scheda video serie GTX 1000 o sucessive.</string>
<string name="suffix_osc_opacity">%</string>
</resources>
+13 -4
View File
@@ -90,8 +90,6 @@
<string name="summary_checkbox_disable_warnings">ストリーミング中に画面に警告メッセージを表示しない</string>
<string name="category_audio_settings">音声</string>
<string name="title_checkbox_51_surround">5.1chサラウンド</string>
<string name="summary_checkbox_51_surround">音声に問題が生じる場合はチェックを外してください。バージョン2.7以降のGFEが必要です</string>
<string name="title_checkbox_multi_controller">複数のゲームコントローラ</string>
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
@@ -107,8 +105,6 @@
<string name="category_ui_settings">インターフェース</string>
<string name="title_language_list">言語</string>
<string name="summary_language_list">Moonlightで使用する言語</string>
<string name="title_checkbox_list_mode">リストメニュー</string>
<string name="summary_checkbox_list_mode">ゲームをグリッドではなく、リストで表示します</string>
<string name="title_checkbox_small_icon_mode">小さなアイコン</string>
<string name="summary_checkbox_small_icon_mode">グリッドで小さなアイコンを使用します</string>
@@ -121,5 +117,18 @@
<string name="category_advanced_settings">高度な設定</string>
<string name="title_video_format">H.265</string>
<string name="summary_video_format">H.265は動画に必要な帯域幅を圧縮します。この機能にはなるべく新しいデバイスが必要です</string>
<string name="suffix_osc_opacity">%</string>
<string name="dialog_title_osc_opacity">透過率</string>
<string name="title_osc_opacity">透過率</string>
<string name="summary_osc_opacity">オンスクリーンコントローラの透過率を調整します</string>
<string name="title_only_l3r3">L3 と R3 のみ表示します</string>
<string name="summary_only_l3r3">L3 と R3 以外のボタンを表示しない</string>
<string name="title_reset_osc">オンスクリーンコントローラをデフォルトに戻します</string>
<string name="summary_reset_osc">サイズやレイアウトを戻します</string>
<string name="dialog_title_reset_osc">デフォルトに戻します</string>
<string name="dialog_text_reset_osc">本当にデフォルトに戻しますか?</string>
<string name="toast_reset_osc_success">オンスクリーンコントローラをデフォルトに戻しました</string>
<string name="title_checkbox_vibrate_osc">振動</string>
<string name="summary_checkbox_vibrate_osc">コントローラの振動を真似します</string>
</resources>
+1 -4
View File
@@ -105,8 +105,6 @@
<string name="summary_checkbox_disable_warnings">화면 상의 연결 경고 메세지를 스트리밍 중에 비활성화합니다.</string>
<string name="category_audio_settings">오디오 설정</string>
<string name="title_checkbox_51_surround">5.1 서라운드 사운드 활성화</string>
<string name="summary_checkbox_51_surround">오디오 문제가 발생한다면 체크를 해제하세요. GFE 2.7이나 그 이상 버전이 필요합니다.</string>
<string name="title_checkbox_multi_controller">다중 컨트롤러 지원</string>
<string name="summary_checkbox_multi_controller">이 옵션을 선택하지 않으면 모든 컨트롤러가 하나로 표시됩니다</string>
@@ -122,8 +120,6 @@
<string name="category_ui_settings">UI 설정</string>
<string name="title_language_list">언어</string>
<string name="summary_language_list">Moonlight에서 사용할 언어를 선택합니다.</string>
<string name="title_checkbox_list_mode">그리드(바둑판) 대신 리스트 뷰 사용</string>
<string name="summary_checkbox_list_mode">그리드 대신 리스트로 앱과 PC를 표시합니다.</string>
<string name="title_checkbox_small_icon_mode">작은 아이콘 사용</string>
<string name="summary_checkbox_small_icon_mode">더 많이 표시하기 위해 그리드 표시에서 작은 아이콘을 사용합니다.</string>
@@ -136,5 +132,6 @@
<string name="category_advanced_settings">고급 설정</string>
<string name="title_video_format">H.265 설정 변경</string>
<string name="summary_video_format">H.265는 비디오 대역폭 요구사항을 낮춰주지만 최신 장치가 필요합니다.</string>
<string name="suffix_osc_opacity">%</string>
</resources>
+1 -4
View File
@@ -94,8 +94,6 @@
<string name="summary_checkbox_disable_warnings">Verberg on-screen verbindingswaarschuwingen tijdens het streamen</string>
<string name="category_audio_settings">Geluidsinstellingen</string>
<string name="title_checkbox_51_surround">Gebruik 5.1 surround sound</string>
<string name="summary_checkbox_51_surround">Gebruik dit niet als er problemen zijn met de audio. Vereist GFE 2.7 of hoger.</string>
<string name="title_checkbox_multi_controller">Multi-gamepad support</string>
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
@@ -111,8 +109,6 @@
<string name="category_ui_settings">UI Installingen</string>
<string name="title_language_list">Taal</string>
<string name="summary_language_list">Taal te gebruiken in Moonlight</string>
<string name="title_checkbox_list_mode">Gebruik lijsten in plaats van kolommen</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Gebruik kleine iconen</string>
<string name="summary_checkbox_small_icon_mode">Gebruik kleine iconen in kolom onderdelen zodat meer items tegelijk zichtbaar worden.</string>
@@ -125,5 +121,6 @@
<string name="category_advanced_settings">Geavanceerde Instellingen</string>
<string name="title_video_format">Verander H.265 instellingen</string>
<string name="summary_video_format">H.265 verlaagt video bandbreedte vereisten maar benodigdt een recent apparaat.</string>
<string name="suffix_osc_opacity">%</string>
</resources>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="audio_config_names">
<item>Stereo</item>
<item>Sunet Surround 5.1</item>
<item>Sunet Surround 7.1</item>
</string-array>
<string-array name="decoder_names">
<item>Auto-selectează decodorul</item>
<item>Forțează decodarea Software</item>
<item>Forțează decodarea Hardware</item>
</string-array>
<string-array name="video_format_names">
<item>Folosește H.265 doar dacă e stabil</item>
<item>Folosește H.265 mereu (se poate bloca)</item>
<item>Nu folosi H.265</item>
</string-array>
</resources>
+191
View File
@@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Shortcut strings -->
<string name="scut_deleted_pc">PC șters</string>
<string name="scut_not_paired">PC neîmperecheat</string>
<string name="scut_pc_not_found">PC negăsit</string>
<string name="scut_invalid_uuid">PC-ul este invalid</string>
<string name="scut_invalid_app_id">Aplicația este invalidă</string>
<!-- Help strings -->
<string name="help_loading_title">Ajutor</string>
<string name="help_loading_msg">Se încarcă pagina de ajutor…</string>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Vezi lista de jocuri</string>
<string name="pcview_menu_pair_pc">Împerechează PC-ul</string>
<string name="pcview_menu_unpair_pc">Desperechează</string>
<string name="pcview_menu_send_wol">Trimite o cerere Wake-On-LAN</string>
<string name="pcview_menu_delete_pc">Șterge PC</string>
<string name="pcview_menu_details">Vezi detalii</string>
<!-- Pair messages -->
<string name="pairing">Se împerechează…</string>
<string name="pair_pc_offline">PC-ul nu este accesibil</string>
<string name="pair_pc_ingame">PC-ul rulează un joc acum. Trebuie să închizi jocul pentru a-l putea împerechea.</string>
<string name="pair_pairing_title">Se împerechează</string>
<string name="pair_pairing_msg">Te rugăm să introduci urmatorul PIN în PC-ul pe care îl împerechezi:</string>
<string name="pair_incorrect_pin">PIN-ul este greșit</string>
<string name="pair_fail">Împerecherea a eșuat</string>
<string name="pair_already_in_progress">Împerecherea este deja în curs</string>
<!-- WOL messages -->
<string name="wol_pc_online">PC-ul este accesibil</string>
<string name="wol_no_mac">Nu s-a putut porni PC-ul deoarece GFE nu a comunicat o adresa MAC</string>
<string name="wol_waking_pc">Se pornește PC-ul…</string>
<string name="wol_waking_msg">Poate dura puțin până PC-ul pornește. Dacă nu pornește, verifică dacă este configurat corect pentru Wake-On-LAN.</string>
<string name="wol_fail">Nu s-au putut trimite pachetele Wake-On-LAN</string>
<!-- Unpair messages -->
<string name="unpairing">Desperecherechere…</string>
<string name="unpair_success">Desperecherechere efectuată cu succes</string>
<string name="unpair_fail">Desperecherea a eșuat</string>
<string name="unpair_error">Dispozitivul nu este împerecheat</string>
<!-- Errors -->
<string name="error_pc_offline">PC-ul este inaccesibil</string>
<string name="error_manager_not_running">Serviciul ComputerManager nu este pornit. Te rugăm să aștepți câteva secunde sau să repornești aplicația.</string>
<string name="error_unknown_host">Nu am putut identifica hostul</string>
<string name="error_404">GFE a returnat un cod de eroare HTTP 404. Asigură-te ca PC-ul are o placa video suportată.
Este posibil sa apară această eroare daca folosești o alta aplicație de remote desktop. Încearcă să repornești PC-ul sau să reinstalezi GFE.
</string>
<string name="title_decoding_error">Decodorul video s-a închis în mod neașteptat</string>
<string name="message_decoding_error">Moonlight s-a închis în mod neașteptat datorită unei incompatibilități cu decodorul video al acestui dispozitiv. Asigură-te ca folosești ultima versiune de GFE. Dacă problema persistă, ajustează setările de streaming.</string>
<string name="title_decoding_reset">Resetează setările video</string>
<string name="message_decoding_reset">Decodorul video al acestui dispozitiv continuă să se blocheze folosind setările video curente. Au fost resetate cele implicite.</string>
<string name="error_usb_prohibited">Accesul USB este interzis de către administratorul dispozitivului. Verifică setarile Knox sau MDM.</string>
<string name="unable_to_pin_shortcut">Launcher-ul tău curent nu permite crearea de scurtături fixate.</string>
<string name="video_decoder_init_failed">Inițializarea decodorului video a eșuat. Este posibil ca acest dispozitiv să nu suporte rezoluția sau rata cadrelor selectată.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Se stabilește conexiunea</string>
<string name="conn_establishing_msg">Se pornește conexiunea</string>
<string name="conn_metered">Atenție: Conexiunea ta curentă este contorizată!</string>
<string name="conn_client_latency">Latența medie a decodării cadrelor:</string>
<string name="conn_client_latency_hw">latența decodorului hardware:</string>
<string name="conn_hardware_latency">Latența medie a decodării cadrelor (hardware):</string>
<string name="conn_starting">Se pornește</string>
<string name="conn_error_title">Eroare la conectare</string>
<string name="conn_error_msg">Pornirea a eșuat</string>
<string name="conn_terminated_title">Conexiunea închisă</string>
<string name="conn_terminated_msg">Conexiunea a fost terminată</string>
<!-- General strings -->
<string name="ip_hint">Adresa IP a PC-ului cu GFE</string>
<string name="searching_pc">Se caută PC-uri cu GameStream activat…\n\n
Asigură-te ca GameStream este activat în setările Geforce Experience SHIELD.</string>
<string name="yes">Da</string>
<string name="no">Nu</string>
<string name="lost_connection">S-a pierdut conexiunea catre PC</string>
<string name="title_details">Detalii</string>
<string name="help">Ajutor</string>
<string name="delete_pc_msg">Sigur dorești să ștergi acest PC?</string>
<string name="slow_connection_msg">Conexiune inceată catre PC\nRedu rata de biți</string>
<string name="poor_connection_msg">Conexiune slabă catre PC</string>
<string name="perf_overlay_text">Dimensiunile video: %1$s\nDecodor: %2$s\nRata cadrelor estimata PC: %3$.2f FPS\nRata cadrelor primite din rețea: %4$.2f FPS\nRata de afisare a cadrelor: %5$.2f FPS\nCadre pierdute de rețea: %6$.2f%%\nTimpul mediu de primire: %7$.2f ms\nTimpul mediu de decodare: %8$.2f ms</string>
<!-- AppList activity -->
<string name="applist_connect_msg">Se conectează la PC…</string>
<string name="applist_menu_resume">Continuă Sesiunea</string>
<string name="applist_menu_quit">Închide Sesiunea</string>
<string name="applist_menu_quit_and_start">Închide Jocul Curent si Pornește</string>
<string name="applist_menu_cancel">Anulează</string>
<string name="applist_menu_details">Vezi detalii</string>
<string name="applist_menu_scut">Creează o scurtătură</string>
<string name="applist_menu_tv_channel">Adaugă la canal</string>
<string name="applist_refresh_title">Lista de aplicații</string>
<string name="applist_refresh_msg">Reîmprospătare aplicații…</string>
<string name="applist_refresh_error_title">Eroare</string>
<string name="applist_refresh_error_msg">Nu s-a putut obține lista de aplicații</string>
<string name="applist_quit_app">Închidere în curs</string>
<string name="applist_quit_success">Închis cu succes</string>
<string name="applist_quit_fail">Nu s-a putut închide lista</string>
<string name="applist_quit_confirmation">Sigur dorești să închizi aplicația curentă? Toate datele nesalvate vor fi pierdute.</string>
<string name="applist_details_id">ID-ul aplicației:</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Adaugă PC manual</string>
<string name="msg_add_pc">Conectare în curs…</string>
<string name="addpc_fail">Nu s-a putut efectua conectarea la adresa introdusă. Asigurăte ca porturile nu sunt blocate in firewall.</string>
<string name="addpc_success">PC adăugat cu succes</string>
<string name="addpc_unknown_host">Nu am putut identifica adresa PC-ului. Asigură-te că ai introdus-o corect.</string>
<string name="addpc_enter_ip">Trebuie să introduci o adresa IP</string>
<string name="addpc_wrong_sitelocal">Adresa introdusă nu pare corectă. Pentru conectare prin Internet, este nevoie de adresa publică a routerului.</string>
<!-- Preferences -->
<string name="category_basic_settings">Setări de bază</string>
<string name="title_resolution_list">Rezolutia video</string>
<string name="summary_resolution_list">Crește-o pentru a îmbunătăți claritatea imaginii. Descrește-o pentru dispozitive neperformante sau conexiune slabă.</string>
<string name="title_fps_list">Rata cadrelor</string>
<string name="summary_fps_list">Crește-o pentru a îmbunătăți fluiditatea imaginilor. Descrește-o pentru dispozitive neperformante sau conexiune slabă.</string>
<string name="title_seekbar_bitrate">Rata de biți</string>
<string name="summary_seekbar_bitrate">Crește-o pentru a îmbunătăți calitatea imaginilor. Descrește-o pentru dispozitive neperformante sau conexiune slabă.</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_unlock_fps">Deblochează toate ratele de cadre posibile</string>
<string name="summary_unlock_fps">Fluxul video de rate mai mari poate reduce latența folosind dispozitive performante, dar poate introduce erori daca nu sunt suportate.</string>
<string name="title_checkbox_stretch_video">Întindeți video pe ecranul complet</string>
<string name="title_checkbox_disable_warnings">Dezactivează mesajele de avertizare</string>
<string name="summary_checkbox_disable_warnings">Dezactivează mesajele de avertizare privind rețeaua în timpul conexiunii</string>
<string name="title_checkbox_enable_pip">Activează modul Picture-In-Picture</string>
<string name="summary_checkbox_enable_pip">Permite vizualizarea (dar nu și controlul) când efectuezi multitasking</string>
<string name="category_audio_settings">Setări Audio</string>
<string name="title_audio_config_list">Configurarea sunetului surround</string>
<string name="summary_audio_config_list">Activeaza sunetul 5.1 sau 7.1 pentru sisteme home-theater</string>
<string name="category_input_settings">Setări de control</string>
<string name="title_checkbox_multi_controller">Detectează automat prezența controllerelor.</string>
<string name="summary_checkbox_multi_controller">Dezactivarea acestei opțiuni implică prezența constantă a unui controller</string>
<string name="title_checkbox_vibrate_fallback">Simuleaza efectul de vibratie</string>
<string name="summary_checkbox_vibrate_fallback">Dacă controllerul nu suportă vibrații, va vibra dispozitivul în schimb.</string>
<string name="title_seekbar_deadzone">Zona moartă a stickului analogic</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Driver pentru controllerele de Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Activează un driver USB pentru dispozitivele fără suport nativ pentru controllere Xbox</string>
<string name="title_checkbox_usb_bind_all">Inlocuiește driverul implicit pentru controllere</string>
<string name="summary_checkbox_usb_bind_all">Forțează driverul USB Moonlight să preia toate controllerele Xbox suportate</string>
<string name="title_checkbox_mouse_emulation">Simulează mouse cu controllerul</string>
<string name="summary_checkbox_mouse_emulation">Apăsarea lungă pe butonul Start schimba modul de operare a controllerului în modul mouse.</string>
<string name="title_checkbox_mouse_nav_buttons">Activează butoanele de înainte și înapoi ale mousului</string>
<string name="summary_checkbox_mouse_nav_buttons">Această opțiune poate afecta click dreapta pentru unele dispozitive problematice.</string>
<string name="category_on_screen_controls_settings">Setări ale controalelor pe ecran</string>
<string name="title_checkbox_show_onscreen_controls">Afișează controale pe ecran</string>
<string name="summary_checkbox_show_onscreen_controls">Afișează un controller virtual pe ecran</string>
<string name="title_checkbox_vibrate_osc">Activează vibrațiile</string>
<string name="summary_checkbox_vibrate_osc">Dispozitivul va vibra asemănător unui controller</string>
<string name="title_only_l3r3">Afișează doar L3 si R3</string>
<string name="summary_only_l3r3">Ascunde toate butoanele în afară de L3 and R3</string>
<string name="title_reset_osc">Șterge schema salvată a controalelor</string>
<string name="summary_reset_osc">Resetează toate controalele de pe ecran la poziția și dimensiunea implicită</string>
<string name="dialog_title_reset_osc">Resetarea schemei de controale</string>
<string name="dialog_text_reset_osc">Sigur dorești să ștergi schema salvată a controalelor de pe ecran?</string>
<string name="toast_reset_osc_success">Controalele de pe ecran au fost resetate la setarile implicite</string>
<string name="title_osc_opacity">Modifică opacitatea controalelor de pe ecran</string>
<string name="summary_osc_opacity">Ajustează gradul de transparență al controalelor de pe ecran</string>
<string name="dialog_title_osc_opacity">Modifică opacitatea</string>
<string name="suffix_osc_opacity">%</string>
<string name="category_ui_settings">Setari UI</string>
<string name="title_language_list">Limba (Language)</string>
<string name="summary_language_list">Limba folosită de către Moonlight</string>
<string name="title_checkbox_small_icon_mode">Folosește iconițe mici</string>
<string name="summary_checkbox_small_icon_mode">Iconițele folosite în grile vor fi mici pentru a încăpea mai multe odata</string>
<string name="category_host_settings">Setările PC-ului gazdă</string>
<string name="title_checkbox_enable_sops">Optimizarea setărilor de joc</string>
<string name="summary_checkbox_enable_sops">Permite GFE să modifice setările jocurilor pentru experiența optimă</string>
<string name="title_checkbox_host_audio">Redă audio si pe PC</string>
<string name="summary_checkbox_host_audio">Sunetul se va auzi atat pe acest dispozitiv cât și pe PC</string>
<string name="category_advanced_settings">Setări avansate</string>
<string name="title_disable_frame_drop">Nu pierde cadre intenționat</string>
<string name="summary_disable_frame_drop">Poate să reducă micro-stuttering pe anumite device-uri, dar s-ar putea să crească latența</string>
<string name="title_video_format">Modifica setările H.265</string>
<string name="summary_video_format">H.265 funcționează cu o conexiune mai slaba, dar necesită un dispozitiv recent, performant</string>
<string name="title_enable_hdr">Activează HDR (Experimental)</string>
<string name="summary_enable_hdr">Folosește HDR daca aplicația si placa video suportă. Necesită o placa video seria GTX 1000 sau mai nouă.</string>
<string name="title_enable_perf_overlay">Activează statisticile de performanță</string>
<string name="summary_enable_perf_overlay">Afișează în timp real statisticile de performanță ale conexiunii.</string>
</resources>
+1 -4
View File
@@ -96,8 +96,6 @@
<string name="summary_checkbox_disable_warnings">Выключить экранные предупреждения о соединении во время трансляции</string>
<string name="category_audio_settings">Аудио Настройки</string>
<string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string>
<string name="summary_checkbox_51_surround">Отключите, если появляются аудио проблемы. Требуется GFE 2.7 или выше.</string>
<string name="title_checkbox_multi_controller">Поддержка нескольких контроллеров</string>
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один</string>
@@ -109,8 +107,6 @@
<string name="category_ui_settings">Настройки Интерфейса</string>
<string name="title_language_list">Язык</string>
<string name="summary_language_list">Язык, который будет использоваться в Moonlight</string>
<string name="title_checkbox_list_mode">Использовать списки вместо сеток</string>
<string name="summary_checkbox_list_mode">Выводить приложения и компьютеры списком, вместо использования сетки</string>
<string name="title_checkbox_small_icon_mode">Использовать маленькие иконки</string>
<string name="summary_checkbox_small_icon_mode">Использовать маленькие иконки в сетке для отображения большего числа элементов на экране</string>
@@ -183,4 +179,5 @@
<string name="summary_fps_list">Увеличение для более плавного видео потока. Уменьшите для лучшей производительности на более слабых устройствах.</string>
<string name="scut_invalid_uuid">Указанный PC недействителен</string>
<string name="scut_invalid_app_id">Указанное приложение недействительно</string>
<string name="suffix_osc_opacity">%</string>
</resources>
+9 -3
View File
@@ -1,15 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="decoder_names">
<string-array name="decoder_names">
<item>自动选择解码器</item>
<item>强制软解</item>
<item>强制硬解</item>
</string-array>
</string-array>
<string-array name="video_format_names">
<string-array name="video_format_names">
<item>如果稳定才使用H.265</item>
<item>强制使用H.265(不稳定)</item>
<item>不使用H.265</item>
</string-array>
<string-array name="audio_config_names">
<item>立体声</item>
<item>5.1环绕声</item>
<item>7.1环绕声</item>
</string-array>
</resources>
+12 -6
View File
@@ -134,10 +134,12 @@
<string name="summary_checkbox_enable_pip">允许多任务时观看串流画面(但不操作)</string>
<string name="category_audio_settings"> 音频设置 </string>
<string name="title_checkbox_51_surround"> 启用 5.1 环绕音效 </string>
<string name="summary_checkbox_51_surround"> 如果你的声音听起来有问题请禁用。 \n 需要GFE 2.7或更高版本 </string>
<string name="title_audio_config_list"> 环绕声设置 </string>
<string name="summary_audio_config_list"> 为家庭影院系统启用5.1或7.1环绕声 </string>
<string name="category_input_settings">输入设置</string>
<string name="category_input_settings">输入设置</string>
<string name="title_checkbox_touchscreen_trackpad">将触控屏作为触控板使用</string>
<string name="summary_checkbox_touchscreen_trackpad">如果启用,则将触控屏作为触控板使用。 如果禁止,则触控屏直接控制鼠标光标</string>
<string name="title_checkbox_multi_controller"> 自动检测手柄 </string>
<string name="summary_checkbox_multi_controller"> 禁用此项所有手柄将视为一个手柄 </string>
<string name="title_checkbox_vibrate_fallback"> 用设备震动模拟游戏震动效果 </string>
@@ -151,7 +153,9 @@
<string name="title_checkbox_mouse_emulation"> 通过手柄模拟鼠标 </string>
<string name="summary_checkbox_mouse_emulation"> 长按开始键将手柄切换为鼠标模式 </string>
<string name="title_checkbox_mouse_nav_buttons"> 启用前进后退鼠标键 </string>
<string name="summary_checkbox_mouse_nav_buttons"> 在一些支持不佳的设备上启用此项可能会使其右键失效 </string>
<string name="summary_checkbox_mouse_nav_buttons"> 在一些支持不佳的设备上启用此项可能会使其右键失效 </string>
<string name="title_checkbox_flip_face_buttons"> 反转技能键 </string>
<string name="summary_checkbox_flip_face_buttons"> 为手柄和虚拟手柄调转A/B和X/Y技能键 </string>
<string name="category_on_screen_controls_settings"> 屏幕控制按钮设置 </string>
<string name="title_checkbox_show_onscreen_controls"> 显示屏幕控制按钮 </string>
@@ -169,8 +173,6 @@
<string name="category_ui_settings"> 界面设置 </string>
<string name="title_language_list"> 语言 </string>
<string name="summary_language_list"> 选择Moonlight显示的语言 </string>
<string name="title_checkbox_list_mode"> 使用列表代替图标 </string>
<string name="summary_checkbox_list_mode"> 列表显示电脑和游戏 </string>
<string name="title_checkbox_small_icon_mode"> 使用小图标 </string>
<string name="summary_checkbox_small_icon_mode"> 使用小图标以在屏幕上显示更多项目 </string>
@@ -205,6 +207,10 @@
<string name="title_enable_perf_overlay"> 启用性能信息 </string>
<string name="summary_enable_perf_overlay"> 在串流中显示实时性能信息 </string>
<string name="title_osc_opacity">更改屏幕按钮透明度</string>
<string name="dialog_title_osc_opacity">透明度</string>
<string name="suffix_osc_opacity">%</string>
<string name="summary_osc_opacity">令屏幕按钮变得更透明/更不透明</string>
</resources>
+9 -3
View File
@@ -1,15 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="decoder_names">
<string-array name="decoder_names">
<item>自動選擇解碼器</item>
<item>強制軟解</item>
<item>強制硬解</item>
</string-array>
</string-array>
<string-array name="video_format_names">
<string-array name="video_format_names">
<item>如果穩定才使用H.265</item>
<item>強制使用H.265(不穩定)</item>
<item>不使用H.265</item>
</string-array>
<string-array name="audio_config_names">
<item>身歷聲</item>
<item>5.1環繞聲</item>
<item>7.1環繞聲</item>
</string-array>
</resources>
+12 -6
View File
@@ -134,10 +134,12 @@
<string name="summary_checkbox_enable_pip">允許多工時觀看串流畫面(但不操作)</string>
<string name="category_audio_settings"> 音訊設置 </string>
<string name="title_checkbox_51_surround"> 啟用 5.1 環繞音效 </string>
<string name="summary_checkbox_51_surround"> 如果你的聲音聽起來有問題請禁用。 \n 需要GFE 2.7或更高版本 </string>
<string name="title_audio_config_list"> 環繞聲設置 </string>
<string name="summary_audio_config_list"> 為家庭劇院系統啟用5.1或7.1環繞聲 </string>
<string name="category_input_settings">輸入設置</string>
<string name="category_input_settings"> 輸入設置 </string>
<string name="title_checkbox_touchscreen_trackpad"> 將觸控屏作為觸控板使用 </string>
<string name="summary_checkbox_touchscreen_trackpad"> 如果啟用,則將觸控屏作為觸控板使用。 如果禁止,則觸控屏直接控制滑鼠游標 </string>
<string name="title_checkbox_multi_controller"> 自動檢測手柄 </string>
<string name="summary_checkbox_multi_controller"> 禁用此項所有手柄將視為一個手柄 </string>
<string name="title_checkbox_vibrate_fallback"> 用設備震動類比遊戲震動效果 </string>
@@ -151,7 +153,9 @@
<string name="title_checkbox_mouse_emulation"> 通過手柄類比滑鼠 </string>
<string name="summary_checkbox_mouse_emulation"> 長按開始鍵將手柄切換為滑鼠模式 </string>
<string name="title_checkbox_mouse_nav_buttons"> 啟用前進後退滑鼠鍵 </string>
<string name="summary_checkbox_mouse_nav_buttons"> 在一些支援不佳的設備上啟用此項可能會使其右鍵失效 </string>
<string name="summary_checkbox_mouse_nav_buttons"> 在一些支援不佳的設備上啟用此項可能會使其右鍵失效 </string>
<string name="title_checkbox_flip_face_buttons">反轉技能鍵</string>
<string name="summary_checkbox_flip_face_buttons">為手柄和虛擬手柄調轉A/B和X/Y技能鍵</string>
<string name="category_on_screen_controls_settings"> 螢幕控制按鈕設置 </string>
<string name="title_checkbox_show_onscreen_controls"> 顯示幕幕控制按鈕 </string>
@@ -169,8 +173,6 @@
<string name="category_ui_settings"> 介面設置 </string>
<string name="title_language_list"> 語言 </string>
<string name="summary_language_list"> 選擇Moonlight顯示的語言 </string>
<string name="title_checkbox_list_mode"> 使用清單代替圖示 </string>
<string name="summary_checkbox_list_mode"> 清單顯示電腦和遊戲 </string>
<string name="title_checkbox_small_icon_mode"> 使用小圖示 </string>
<string name="summary_checkbox_small_icon_mode"> 使用小圖示以在螢幕上顯示更多專案 </string>
@@ -205,6 +207,10 @@
<string name="title_enable_perf_overlay"> 啟用性能資訊 </string>
<string name="summary_enable_perf_overlay"> 在串流中顯示即時性能資訊 </string>
<string name="title_osc_opacity">更改屏幕按鈕透明度</string>
<string name="dialog_title_osc_opacity">透明度</string>
<string name="suffix_osc_opacity">%</string>
<string name="summary_osc_opacity">令屏幕按钮變得更透明/更不透明</string>
</resources>
+19 -6
View File
@@ -9,12 +9,12 @@
<item>4K</item>
</string-array>
<string-array name="resolution_values" translatable="false">
<item>360p</item>
<item>480p</item>
<item>720p</item>
<item>1080p</item>
<item>1440p</item>
<item>4K</item>
<item>640x360</item>
<item>854x480</item>
<item>1280x720</item>
<item>1920x1080</item>
<item>2560x1440</item>
<item>3840x2160</item>
</string-array>
<string-array name="fps_names">
@@ -30,6 +30,17 @@
<item>120</item>
</string-array>
<string-array name="audio_config_names">
<item>Stereo</item>
<item>5.1 Surround Sound</item>
<item>7.1 Surround Sound</item>
</string-array>
<string-array name="audio_config_values" translatable="false">
<item>2</item>
<item>51</item>
<item>71</item>
</string-array>
<string-array name="language_names" translatable="false">
<item>Default</item>
<item>English</item>
@@ -43,6 +54,7 @@
<item>Español</item>
<item>Français</item>
<item>Deutsch</item>
<item>Română</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
@@ -57,6 +69,7 @@
<item>es</item>
<item>fr</item>
<item>de</item>
<item>ro</item>
</string-array>
<string-array name="decoder_names">
+28 -18
View File
@@ -60,6 +60,8 @@
<string name="message_decoding_reset">Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default.</string>
<string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
<string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string>
<string name="video_decoder_init_failed">Video decoder failed to initialize. Your device may not support the selected resolution or frame rate.</string>
<string name="no_video_received_error">No video received from host. Check the host PC\'s firewall and port forwarding rules.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
@@ -125,33 +127,31 @@
<string name="title_seekbar_bitrate">Video bitrate</string>
<string name="summary_seekbar_bitrate">Increase for better image quality. Decrease to improve performance on slower connections.</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_unlock_fps">Unlock all possible frame rates</string>
<string name="summary_unlock_fps">Streaming at 90 or 120 FPS may reduce latency on high-end devices but can cause lag or crashes on devices that can\'t support it</string>
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="title_checkbox_enable_pip">Enable Picture-in-Picture observer mode</string>
<string name="summary_checkbox_enable_pip">Allows the stream to be viewed (but not controlled) while multitasking</string>
<string name="category_audio_settings">Audio Settings</string>
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
<string name="summary_checkbox_51_surround">Uncheck if you experience audio issues. Requires GFE 2.7 or higher.</string>
<string name="title_audio_config_list">Surround sound configuration</string>
<string name="summary_audio_config_list">Enable 5.1 or 7.1 surround sound for home-theater systems</string>
<string name="category_input_settings">Input Settings</string>
<string name="title_checkbox_touchscreen_trackpad">Use the touchscreen as a trackpad</string>
<string name="summary_checkbox_touchscreen_trackpad">If enabled, the touchscreen acts like a trackpad. If disabled, the touchscreen directly controls the mouse cursor.</string>
<string name="title_checkbox_multi_controller">Automatic gamepad presence detection</string>
<string name="summary_checkbox_multi_controller">Unchecking this option forces a gamepad to always be present</string>
<string name="title_checkbox_vibrate_fallback">Emulate rumble support with vibration</string>
<string name="summary_checkbox_vibrate_fallback">Vibrates your device to emulate rumble if your gamepad does not support it</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One USB gamepad driver</string>
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox controller support</string>
<string name="title_checkbox_usb_bind_all">Override Android controller support</string>
<string name="summary_checkbox_usb_bind_all">Forces Moonlight\'s USB driver to take over all supported Xbox gamepads</string>
<string name="title_checkbox_usb_bind_all">Override native Xbox gamepad support</string>
<string name="summary_checkbox_usb_bind_all">Use Moonlight\'s USB driver for all supported gamepads, even if native Xbox controller support is present</string>
<string name="title_checkbox_mouse_emulation">Mouse emulation via gamepad</string>
<string name="summary_checkbox_mouse_emulation">Long pressing the Start button will switch the gamepad into mouse mode</string>
<string name="title_checkbox_mouse_nav_buttons">Enable back and forward mouse buttons</string>
<string name="summary_checkbox_mouse_nav_buttons">Enabling this option may break right clicking on some buggy devices</string>
<string name="title_checkbox_flip_face_buttons">Flip face buttons</string>
<string name="summary_checkbox_flip_face_buttons">Switches the face buttons A/B and X/Y for gamepads and the on-screen controls</string>
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
@@ -165,14 +165,18 @@
<string name="dialog_title_reset_osc">Reset Layout</string>
<string name="dialog_text_reset_osc">Are you sure you want to delete your saved on-screen controls layout?</string>
<string name="toast_reset_osc_success">On-screen controls reset to default</string>
<string name="title_osc_opacity">Change opacity of on-screen controls</string>
<string name="summary_osc_opacity">Make the on-screen controls more/less transparent</string>
<string name="dialog_title_osc_opacity">Change opacity</string>
<string name="suffix_osc_opacity">%</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_checkbox_enable_pip">Enable Picture-in-Picture observer mode</string>
<string name="summary_checkbox_enable_pip">Allows the stream to be viewed (but not controlled) while multitasking</string>
<string name="title_language_list">Language</string>
<string name="summary_language_list">Language to use for Moonlight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
<string name="title_checkbox_small_icon_mode">Use small box art</string>
<string name="summary_checkbox_small_icon_mode">Smaller box art in the app grid allows more apps to be visible on screen</string>
<string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string>
@@ -181,13 +185,19 @@
<string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
<string name="category_advanced_settings">Advanced Settings</string>
<string name="title_unlock_fps">Unlock all possible frame rates</string>
<string name="summary_unlock_fps">Streaming at 90 or 120 FPS may reduce latency on high-end devices but can cause lag or instability on devices that can\'t support it</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="title_disable_frame_drop">Never drop frames</string>
<string name="summary_disable_frame_drop">May reduce micro-stuttering on some devices, but can increase latency</string>
<string name="title_video_format">Change H.265 settings</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a newer device</string>
<string name="title_enable_hdr">Enable HDR (Experimental)</string>
<string name="summary_enable_hdr">Stream HDR when the game and PC GPU support it. HDR requires a GTX 1000 series GPU or later.</string>
<string name="title_enable_perf_overlay">Enable performance overlay</string>
<string name="summary_enable_perf_overlay">Display an on-screen overlay with real-time performance information while streaming</string>
<string name="title_enable_perf_overlay">Show performance stats while streaming</string>
<string name="summary_enable_perf_overlay">Display real-time stream performance information while streaming</string>
<string name="title_enable_post_stream_toast">Show latency message after streaming</string>
<string name="summary_enable_post_stream_toast">Display a latency information message after the stream ends</string>
</resources>
+48 -23
View File
@@ -10,7 +10,7 @@
android:summary="@string/summary_resolution_list"
android:entries="@array/resolution_names"
android:entryValues="@array/resolution_values"
android:defaultValue="720p" />
android:defaultValue="1280x720" />
<ListPreference
android:key="list_fps"
android:title="@string/title_fps_list"
@@ -27,35 +27,33 @@
android:summary="@string/summary_seekbar_bitrate"
android:text="@string/suffix_seekbar_bitrate"
android:title="@string/title_seekbar_bitrate" />
<CheckBoxPreference
android:key="checkbox_unlock_fps"
android:title="@string/title_unlock_fps"
android:summary="@string/summary_unlock_fps"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_stretch_video"
android:title="@string/title_checkbox_stretch_video"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_enable_pip"
android:title="@string/title_checkbox_enable_pip"
android:summary="@string/summary_checkbox_enable_pip"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_audio_settings">
<CheckBoxPreference
android:key="checkbox_51_surround"
android:title="@string/title_checkbox_51_surround"
android:summary="@string/summary_checkbox_51_surround"
android:defaultValue="false" />
<ListPreference
android:key="list_audio_config"
android:title="@string/title_audio_config_list"
android:summary="@string/summary_audio_config_list"
android:entries="@array/audio_config_names"
android:entryValues="@array/audio_config_values"
android:defaultValue="2" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_input_settings">
<PreferenceCategory android:title="@string/category_input_settings"
android:key="category_input_settings">
<!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone"
android:defaultValue="15"
android:max="50"
android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/-->
<CheckBoxPreference
android:key="checkbox_touchscreen_trackpad"
android:title="@string/title_checkbox_touchscreen_trackpad"
android:summary="@string/summary_checkbox_touchscreen_trackpad"
android:defaultValue="true" />
<CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
@@ -87,6 +85,11 @@
android:title="@string/title_checkbox_vibrate_fallback"
android:summary="@string/summary_checkbox_vibrate_fallback"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_flip_face_buttons"
android:title="@string/title_checkbox_flip_face_buttons"
android:summary="@string/summary_checkbox_flip_face_buttons"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
android:key="category_onscreen_controls">
@@ -107,6 +110,17 @@
android:key="checkbox_only_show_L3R3"
android:summary="@string/summary_only_l3r3"
android:title="@string/title_only_l3r3" />
<com.limelight.preferences.SeekBarPreference
android:key="seekbar_osc_opacity"
android:dependency="checkbox_show_onscreen_controls"
android:dialogMessage="@string/summary_osc_opacity"
seekbar:min="0"
seekbar:step="1"
android:max="100"
android:defaultValue="90"
android:summary="@string/summary_osc_opacity"
android:text="@string/suffix_osc_opacity"
android:title="@string/dialog_title_osc_opacity" />
<com.limelight.preferences.ConfirmDeleteOscPreference
android:title="@string/title_reset_osc"
android:summary="@string/summary_reset_osc"
@@ -129,7 +143,13 @@
android:summary="@string/summary_checkbox_host_audio"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_ui_settings">
<PreferenceCategory android:title="@string/category_ui_settings"
android:key="category_ui_settings">
<CheckBoxPreference
android:key="checkbox_enable_pip"
android:title="@string/title_checkbox_enable_pip"
android:summary="@string/summary_checkbox_enable_pip"
android:defaultValue="false" />
<ListPreference
android:key="list_languages"
android:title="@string/title_language_list"
@@ -141,14 +161,14 @@
android:key="checkbox_small_icon_mode"
android:title="@string/title_checkbox_small_icon_mode"
android:summary="@string/summary_checkbox_small_icon_mode" />
<CheckBoxPreference
android:key="checkbox_list_mode"
android:title="@string/title_checkbox_list_mode"
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"
android:key="category_advanced_settings">
<CheckBoxPreference
android:key="checkbox_unlock_fps"
android:title="@string/title_unlock_fps"
android:summary="@string/summary_unlock_fps"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_disable_warnings"
android:title="@string/title_checkbox_disable_warnings"
@@ -176,5 +196,10 @@
android:title="@string/title_enable_perf_overlay"
android:summary="@string/summary_enable_perf_overlay"
android:defaultValue="false"/>
<CheckBoxPreference
android:key="checkbox_enable_post_stream_toast"
android:title="@string/title_enable_post_stream_toast"
android:summary="@string/summary_enable_post_stream_toast"
android:defaultValue="false"/>
</PreferenceCategory>
</PreferenceScreen>
+1 -1
View File
@@ -5,7 +5,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.android.tools.build:gradle:4.0.0'
}
}
@@ -0,0 +1,6 @@
- Latency optimizations for Android R DP1
- Video decoder optimizations
- Added Start+Back+LB+RB combo to disconnect the stream
- Fixed back button on some Android TV remotes
- Fixed d-pad on gamepads that expose non-functional hat axes
- Fixed unexpected mouse input after using gamepad mouse emulation mode
@@ -0,0 +1,4 @@
- Configurable transparency for on-screen controls
- Fixed a crash when pinning an app shortcut to the home screen
- Fixed right click unexpectedly stopping the stream on some devices
- Improved key repeating behavior while streaming
@@ -0,0 +1,2 @@
- Improved performance during periods of packet loss
- Improved prioritization of control data sent back to the PC
@@ -0,0 +1,2 @@
- Added 7.1 surround sound support
- Fixed a crash on some devices during a high intensity rumble event
@@ -0,0 +1,4 @@
- Improved support for GFE 3.20.3.63
- Audio performance optimization
- Added Romanian translation
- Updated Simplified Chinese and Traditional Chinese translations
@@ -0,0 +1,5 @@
- Improved stylus support including direct mouse control
- Improved mouse support for ChromeOS and Samsung DeX
- Improved mouse support for devices running Android 7.0 and earlier
- Improved mapping for Start and Select on the ROG Kunai
- Fixed a crash when GeForce Experience returns an invalid status code value
@@ -0,0 +1,3 @@
- Improved stylus deadzone behavior
- Fixed incorrect mouse position on styluses without hover support
- Fixed mouse clicks causing cursor jumping on Nvidia Shield devices
@@ -0,0 +1,5 @@
- Direct touchscreen mouse control is now supported! (disable touchscreen trackpad mode in settings)
- Added an option to switch the A/B and X/Y face buttons on gamepads and on-screen controls
- Added a special error message if no video was received instead of just displaying a black screen
- Improved support for Xbox One S controllers using the new 4.8 firmware
- Improved handling of audio capture errors reported by GeForce Experience
@@ -0,0 +1,4 @@
- Fixed stream starting in 720p when configured for 1080p or 4K at 90 or 120 FPS
- Fixed incorrect behavior of right-side modifier keys while streaming
- Fixed the mouse cursor not reaching the bottom and right edges of the host's screen on some devices
- Updated Traditional and Simplified Chinese translations
@@ -0,0 +1,6 @@
- Improved PC and App Grid UI
- Android 11 optimizations
- Added mapping for Nintendo Switch Pro controller
- Post-stream latency message is now disabled by default
- Fixed control stream connection error on some networks
- Reorganized some of the settings options
@@ -6,20 +6,21 @@ Streaming performance may vary based on your client device and network setup. HD
* Open-source and completely free (no ads, IAPs, or "Pro")
* Streams games purchased from any store
* Works on your home network or over the Internet/LTE
* Up to 4K 120 FPS HDR streaming with 5.1 surround sound
* Keyboard and mouse support (with Android 8.0 or rooted device)
* Up to 4K 120 FPS HDR streaming with 7.1 surround sound
* Keyboard and mouse support (best with Android 8.0 or later)
* Stylus/S-Pen support
* Supports PS3, PS4, Xbox 360, Xbox One, and Android gamepads
* Force feedback support
* Local co-op with up to 4 connected controllers
* Mouse control via gamepad by long-pressing Start
'''PC Requirements'''
* NVIDIA GeForce GTX/RTX series GPU (''GT-series and AMD GPUs aren't supported by NVIDIA GameStream'')
* NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU
* Windows 7 or later
* NVIDIA GeForce Experience (GFE) 2.2.2 or later
* NVIDIA GeForce Experience or NVIDIA Quadro Experience installed
'''Quick Setup Instructions'''
# Make sure GeForce Experience is open on your PC. Turn on GameStream in the SHIELD settings page.
# Make sure GeForce/Quadro Experience is open on your PC. Turn on GameStream in the SHIELD settings page.
# Tap on the PC in Moonlight and type the PIN on your PC
# Start streaming!
+2 -2
View File
@@ -1,6 +1,6 @@
#Tue Aug 20 11:37:45 PDT 2019
#Thu May 28 11:41:09 PDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
+1 -1
View File
@@ -1 +1 @@
include ':app', ':moonlight-common'
include ':app'