mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-02-16 10:31:07 +00:00
Compare commits
215 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc7ac79fa6 | ||
|
|
4c5c27dfc1 | ||
|
|
4aabfbd52e | ||
|
|
6eab842361 | ||
|
|
b729dfd702 | ||
|
|
6366840781 | ||
|
|
704a2ee90b | ||
|
|
484be9bfe6 | ||
|
|
a99e070c26 | ||
|
|
bf803f88af | ||
|
|
9af6febca5 | ||
|
|
0101d0a1bd | ||
|
|
266874609d | ||
|
|
2ba7feedfc | ||
|
|
43c67b4939 | ||
|
|
2d9915e43a | ||
|
|
2329b41bce | ||
|
|
536496184e | ||
|
|
429c32477c | ||
|
|
f5d51b2061 | ||
|
|
2ad1aaa277 | ||
|
|
3afd32dbc1 | ||
|
|
092830ed07 | ||
|
|
d118a6d3ff | ||
|
|
fe97ffdc2f | ||
|
|
964d2ce59c | ||
|
|
dc52684cbc | ||
|
|
191bedc56f | ||
|
|
47b2ace7fd | ||
|
|
9fb7359a3e | ||
|
|
4a5de26406 | ||
|
|
6fa18e126f | ||
|
|
1149002e0c | ||
|
|
d704cb0b50 | ||
|
|
d59e5ae9cf | ||
|
|
4587c1550d | ||
|
|
b5bd329ada | ||
|
|
beccd7a4ac | ||
|
|
61262fa939 | ||
|
|
7c6b006631 | ||
|
|
dbd149354a | ||
|
|
4306ba5004 | ||
|
|
6de370b82f | ||
|
|
45781666b8 | ||
|
|
538231eb6f | ||
|
|
eb74f87f2c | ||
|
|
59d71ffdcf | ||
|
|
d1b93d4011 | ||
|
|
d8ddf2e740 | ||
|
|
581327dc8e | ||
|
|
76e4512a0c | ||
|
|
efdd55beca | ||
|
|
2c115649b9 | ||
|
|
2ddcc31a93 | ||
|
|
3bcce5b749 | ||
|
|
80dac27214 | ||
|
|
4a1177d048 | ||
|
|
4725d8f270 | ||
|
|
07b3528515 | ||
|
|
d2d1b1ea26 | ||
|
|
232b897abc | ||
|
|
efd076bc6c | ||
|
|
cc877480ff | ||
|
|
363145a284 | ||
|
|
755571ad33 | ||
|
|
39edb55721 | ||
|
|
15aa7ecc2e | ||
|
|
ce9e91153e | ||
|
|
9ee0a46606 | ||
|
|
20dc351f4c | ||
|
|
c30c54d562 | ||
|
|
45ff51c0d2 | ||
|
|
5b86e99138 | ||
|
|
0c72910eb7 | ||
|
|
3b0f485b41 | ||
|
|
2be2c95212 | ||
|
|
e7aeeb8bd5 | ||
|
|
73df93f86a | ||
|
|
9cd4d5e2aa | ||
|
|
c3b81554f4 | ||
|
|
6f79c52fc5 | ||
|
|
29bc3e022b | ||
|
|
7d03203d83 | ||
|
|
11dde835d1 | ||
|
|
52c47c288c | ||
|
|
63072aa8e1 | ||
|
|
4cca3ac922 | ||
|
|
604bc1ec11 | ||
|
|
5d7fbf3195 | ||
|
|
8c56e6f0d4 | ||
|
|
2069be7932 | ||
|
|
9c1c2991a9 | ||
|
|
81dabf2713 | ||
|
|
27520cb77e | ||
|
|
f555d3dae0 | ||
|
|
70f1a2cacb | ||
|
|
7f15aaa2e5 | ||
|
|
e5726205c4 | ||
|
|
07fabc0663 | ||
|
|
800f97ae85 | ||
|
|
3ee5b284e1 | ||
|
|
c0389f0da9 | ||
|
|
a7a4d7ded5 | ||
|
|
87cd974b79 | ||
|
|
7faaac31ff | ||
|
|
7386eb2a78 | ||
|
|
49a1524f4f | ||
|
|
c957b8b06b | ||
|
|
a3a6e14d80 | ||
|
|
7231f5468b | ||
|
|
4dfb0d7220 | ||
|
|
2f4f53b048 | ||
|
|
b6e8389544 | ||
|
|
d113878613 | ||
|
|
f7ed7e06db | ||
|
|
977a1d4a3c | ||
|
|
eefc08db47 | ||
|
|
ab2b1663d3 | ||
|
|
04b8a718e3 | ||
|
|
37cf260ba6 | ||
|
|
8f91fe4cd1 | ||
|
|
9246ad412f | ||
|
|
1ccbbdd4fb | ||
|
|
16cf37994d | ||
|
|
01e84624c2 | ||
|
|
939cd7cf70 | ||
|
|
4b11603035 | ||
|
|
ca18b6b052 | ||
|
|
3d0d19e561 | ||
|
|
ae463a8735 | ||
|
|
7e797829ae | ||
|
|
431ed6bc5d | ||
|
|
e9bb711c42 | ||
|
|
623bc5c156 | ||
|
|
cfefef4619 | ||
|
|
4a9a881c1f | ||
|
|
13a06d585c | ||
|
|
1c8ad64da0 | ||
|
|
1d8925de57 | ||
|
|
0eb7e779b8 | ||
|
|
a4b86eefe2 | ||
|
|
902a58bc70 | ||
|
|
a34a44f29a | ||
|
|
454fe80172 | ||
|
|
81b6a8a311 | ||
|
|
3011a5bad7 | ||
|
|
dcb7be3acd | ||
|
|
68a6b510b1 | ||
|
|
dca3e89303 | ||
|
|
bae6fef588 | ||
|
|
37f65e43a5 | ||
|
|
8c910101c7 | ||
|
|
112d9c41eb | ||
|
|
c91d1097f6 | ||
|
|
105c2c9eef | ||
|
|
b754d2de28 | ||
|
|
f6425c7ec6 | ||
|
|
e690c9b8c8 | ||
|
|
f87cbac77c | ||
|
|
150bd313cf | ||
|
|
bc90cb894c | ||
|
|
c51a75a681 | ||
|
|
68aa9bd12d | ||
|
|
1fb6bf4d70 | ||
|
|
b4df3658f1 | ||
|
|
4efb4d9b24 | ||
|
|
d61e893731 | ||
|
|
951e44728e | ||
|
|
8dcdf73222 | ||
|
|
44c3b0af57 | ||
|
|
2b295400ac | ||
|
|
aa8d8e93d2 | ||
|
|
89be7eac0e | ||
|
|
f3847b932b | ||
|
|
5e4f37532c | ||
|
|
f3f5ca74a3 | ||
|
|
e50b7076a1 | ||
|
|
36ab5aa1b6 | ||
|
|
a0a2b299d9 | ||
|
|
14d354fc29 | ||
|
|
342515f916 | ||
|
|
5f5944c237 | ||
|
|
c025432ad6 | ||
|
|
171a6437fe | ||
|
|
11b3648fac | ||
|
|
d1fae89d6d | ||
|
|
5c06848fe9 | ||
|
|
b50e506e58 | ||
|
|
59fafa163d | ||
|
|
22d84b5763 | ||
|
|
6d186892a8 | ||
|
|
88d6143897 | ||
|
|
b729fba75e | ||
|
|
c0d3f9fa48 | ||
|
|
af5e7a0e33 | ||
|
|
371d96ea65 | ||
|
|
e9e332ff85 | ||
|
|
e133ac2815 | ||
|
|
1dba5d147e | ||
|
|
1616c0b022 | ||
|
|
bcee2cf0e3 | ||
|
|
3e7ddab0e9 | ||
|
|
5da0177356 | ||
|
|
7e21638811 | ||
|
|
db5b7ab867 | ||
|
|
3bcc1c84bb | ||
|
|
d46053f8d6 | ||
|
|
00a5fed9e9 | ||
|
|
b6315a715a | ||
|
|
0da8303468 | ||
|
|
c821c4684f | ||
|
|
6bae33f822 | ||
|
|
08d4ab67a6 | ||
|
|
62203d2f21 | ||
|
|
fdd4c0bbe1 |
4
.github/auto-comment.yml
vendored
Normal file
4
.github/auto-comment.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
issuesOpened: >
|
||||
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||
Thank you, and happy streaming!
|
||||
@@ -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"
|
||||
|
||||
49
README.md
49
README.md
@@ -2,55 +2,26 @@
|
||||
|
||||
[](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
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
|
||||
versionName "8.3"
|
||||
versionCode = 200
|
||||
versionName "9.6.4"
|
||||
versionCode = 237
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:installLocation="auto"
|
||||
android:gwpAsanMode="always"
|
||||
android:theme="@style/AppTheme">
|
||||
<provider
|
||||
android:name=".PosterContentProvider"
|
||||
@@ -120,7 +121,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" />
|
||||
|
||||
@@ -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
|
||||
@@ -586,7 +590,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
listView.requestFocus();
|
||||
}
|
||||
|
||||
public class AppObject {
|
||||
public static class AppObject {
|
||||
public final NvApp app;
|
||||
public boolean isRunning;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -27,6 +29,7 @@ import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.ui.GameGestures;
|
||||
import com.limelight.ui.StreamView;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.NetHelper;
|
||||
import com.limelight.utils.ShortcutHelper;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
@@ -42,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;
|
||||
@@ -59,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;
|
||||
@@ -72,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;
|
||||
@@ -83,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
|
||||
@@ -94,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;
|
||||
@@ -115,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;
|
||||
@@ -199,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
|
||||
@@ -225,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -397,45 +417,61 @@ 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);
|
||||
if ((!prefConfig.disableFrameDrop || prefConfig.unlockFps) && prefConfig.fps >= roundedRefreshRate) {
|
||||
if (prefConfig.unlockFps) {
|
||||
// Use frame drops when rendering above the screen frame rate
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Using drop mode for FPS > Hz");
|
||||
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,
|
||||
// causing frame pacing issues. See https://issuetracker.google.com/issues/143401475
|
||||
// To work around this, use frame drop mode if we want to stream at >= 60 FPS.
|
||||
if (prefConfig.fps >= 60) {
|
||||
LimeLog.info("Using Pixel 4 rendering hack");
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
}
|
||||
}
|
||||
else if (roundedRefreshRate <= 49) {
|
||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
|
||||
}
|
||||
// HACK: Avoid crashing on some MTK devices
|
||||
else if (roundedRefreshRate == 50 && decoderRenderer.is49FpsBlacklisted()) {
|
||||
// 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);
|
||||
else if (prefConfig.fps >= roundedRefreshRate) {
|
||||
if (prefConfig.unlockFps) {
|
||||
// Use frame drops when rendering above the screen frame rate
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Using drop mode for FPS > Hz");
|
||||
} else if (roundedRefreshRate <= 49) {
|
||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Bogus refresh rate: " + roundedRefreshRate);
|
||||
}
|
||||
// HACK: Avoid crashing on some MTK devices
|
||||
else if (decoderRenderer.isBlacklistedForFrameRate(roundedRefreshRate - 1)) {
|
||||
// Use the old rendering strategy on these broken devices
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
} else {
|
||||
chosenFrameRate = roundedRefreshRate - 1;
|
||||
LimeLog.info("Adjusting FPS target for screen to " + chosenFrameRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean vpnActive = NetHelper.isActiveNetworkVpn(this);
|
||||
if (vpnActive) {
|
||||
LimeLog.info("Detected active network is a VPN");
|
||||
}
|
||||
|
||||
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)
|
||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||
.setMaxPacketSize(1392)
|
||||
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO)
|
||||
.setMaxPacketSize(vpnActive ? 1024 : 1392) // Lower MTU on VPN
|
||||
.setRemoteConfiguration(vpnActive ? // Use remote optimizations on VPN
|
||||
StreamConfiguration.STREAM_CFG_REMOTE :
|
||||
StreamConfiguration.STREAM_CFG_AUTO)
|
||||
.setHevcBitratePercentageMultiplier(75)
|
||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||
.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
|
||||
@@ -447,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
|
||||
@@ -651,6 +692,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
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
|
||||
getWindow().setAttributes(windowLayoutParams);
|
||||
|
||||
@@ -685,7 +731,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||
}
|
||||
|
||||
return displayRefreshRate;
|
||||
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")
|
||||
@@ -794,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
|
||||
@@ -944,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;
|
||||
}
|
||||
|
||||
@@ -972,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;
|
||||
@@ -1002,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;
|
||||
}
|
||||
|
||||
@@ -1064,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;
|
||||
@@ -1078,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) {
|
||||
@@ -1096,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) {
|
||||
@@ -1113,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 {
|
||||
@@ -1122,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 {
|
||||
@@ -1151,36 +1248,58 @@ 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 &&
|
||||
virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) {
|
||||
// Ignore presses when the virtual controller is in configuration mode
|
||||
(virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons ||
|
||||
virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)) {
|
||||
// Ignore presses when the virtual controller is being configured
|
||||
return true;
|
||||
}
|
||||
|
||||
if (view == null && !prefConfig.touchscreenTrackpad) {
|
||||
// Absolute touch events should be dropped outside our view.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1213,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:
|
||||
@@ -1226,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:
|
||||
@@ -1257,6 +1382,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
for (TouchContext aTouchContext : touchContextMap) {
|
||||
aTouchContext.cancelTouch();
|
||||
aTouchContext.setPointerCount(0);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1272,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
|
||||
@@ -1352,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() {
|
||||
@@ -1367,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() {
|
||||
@@ -1395,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();
|
||||
@@ -1517,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
|
||||
|
||||
@@ -5,21 +5,21 @@ import java.util.logging.FileHandler;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class LimeLog {
|
||||
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
|
||||
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
|
||||
|
||||
public static void info(String msg) {
|
||||
LOGGER.info(msg);
|
||||
}
|
||||
|
||||
public static void warning(String msg) {
|
||||
LOGGER.warning(msg);
|
||||
}
|
||||
|
||||
public static void severe(String msg) {
|
||||
LOGGER.severe(msg);
|
||||
}
|
||||
|
||||
public static void setFileHandler(String fileName) throws IOException {
|
||||
LOGGER.addHandler(new FileHandler(fileName));
|
||||
}
|
||||
public static void info(String msg) {
|
||||
LOGGER.info(msg);
|
||||
}
|
||||
|
||||
public static void warning(String msg) {
|
||||
LOGGER.warning(msg);
|
||||
}
|
||||
|
||||
public static void severe(String msg) {
|
||||
LOGGER.severe(msg);
|
||||
}
|
||||
|
||||
public static void setFileHandler(String fileName) throws IOException {
|
||||
LOGGER.addHandler(new FileHandler(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -721,7 +719,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
registerForContextMenu(listView);
|
||||
}
|
||||
|
||||
public class ComputerObject {
|
||||
public static class ComputerObject {
|
||||
public ComputerDetails details;
|
||||
|
||||
public ComputerObject(ComputerDetails details) {
|
||||
|
||||
@@ -14,10 +14,10 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
|
||||
private AudioTrack track;
|
||||
|
||||
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
||||
private AudioTrack createAudioTrack(int channelConfig, int sampleRate, int bufferSize, boolean lowLatency) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
48000,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize,
|
||||
@@ -28,7 +28,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
.setUsage(AudioAttributes.USAGE_GAME);
|
||||
AudioFormat format = new AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setSampleRate(48000)
|
||||
.setSampleRate(sampleRate)
|
||||
.setChannelMask(channelConfig)
|
||||
.build();
|
||||
|
||||
@@ -64,25 +64,46 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int setup(int audioConfiguration) {
|
||||
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 * 240 * 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 * 240 * 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.
|
||||
@@ -122,7 +143,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
case 1:
|
||||
case 3:
|
||||
// Try the larger buffer size
|
||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT),
|
||||
bytesPerFrame * 2);
|
||||
@@ -135,13 +156,13 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// Skip low latency options if hardware sample rate isn't 48000Hz
|
||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
||||
// Skip low latency options if hardware sample rate doesn't match the content
|
||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != sampleRate && lowLatency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
||||
track = createAudioTrack(channelConfig, sampleRate, bufferSize, lowLatency);
|
||||
track.play();
|
||||
|
||||
// Successfully created working AudioTrack. We're done here.
|
||||
@@ -170,14 +191,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
@Override
|
||||
public void playDecodedAudio(short[] audioData) {
|
||||
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
||||
if (MoonBridge.getPendingAudioFrames() < 8) {
|
||||
if (MoonBridge.getPendingAudioDuration() < 40) {
|
||||
// This will block until the write is completed. That can cause a backlog
|
||||
// of pending audio data, so we do the above check to be able to bound
|
||||
// latency at 40 ms in that situation.
|
||||
track.write(audioData, 0, audioData.length);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Too many pending audio frames: " + MoonBridge.getPendingAudioFrames());
|
||||
LimeLog.info("Too much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,9 +102,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
LimeLog.warning("Corrupted certificate");
|
||||
return false;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
// May happen if the key is corrupt
|
||||
LimeLog.warning("Corrupted key");
|
||||
@@ -124,10 +122,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e1) {
|
||||
// Should never happen
|
||||
e1.printStackTrace();
|
||||
return false;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
@@ -152,8 +148,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||
} catch (Exception e) {
|
||||
// Nothing should go wrong here
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -39,19 +40,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
|
||||
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
||||
|
||||
private static final int EMULATING_SPECIAL = 0x1;
|
||||
private static final int EMULATING_SELECT = 0x2;
|
||||
|
||||
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
|
||||
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
|
||||
|
||||
private final Vector2d inputVector = new Vector2d();
|
||||
|
||||
private final SparseArray<InputDeviceContext> inputDeviceContexts = new SparseArray<>();
|
||||
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 +56,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 +103,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 +142,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 +157,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();
|
||||
@@ -215,9 +214,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
if (PreferenceConfiguration.readPreferences(context).usbDriver) {
|
||||
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||
// We explicitly ask not to claim devices that appear as InputDevices
|
||||
// We explicitly check not to claim devices that appear as InputDevices
|
||||
// otherwise we will double count them.
|
||||
if (UsbDriverService.shouldClaimDevice(dev, false)) {
|
||||
if (UsbDriverService.shouldClaimDevice(dev, false) &&
|
||||
!UsbDriverService.isRecognizedInputDevice(dev)) {
|
||||
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
|
||||
mask |= 1 << count++;
|
||||
}
|
||||
@@ -263,9 +263,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;
|
||||
}
|
||||
@@ -326,6 +324,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;
|
||||
@@ -342,6 +344,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();
|
||||
@@ -375,9 +388,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;
|
||||
}
|
||||
|
||||
@@ -400,11 +411,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;
|
||||
}
|
||||
}
|
||||
@@ -416,8 +430,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) {
|
||||
@@ -425,10 +442,20 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
String devName = dev.getName();
|
||||
|
||||
LimeLog.info("Creating controller context for device: "+devName);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
LimeLog.info("Vendor ID: "+dev.getVendorId());
|
||||
LimeLog.info("Product ID: "+dev.getProductId());
|
||||
}
|
||||
LimeLog.info(dev.toString());
|
||||
|
||||
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();
|
||||
@@ -471,25 +498,45 @@ 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 (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
||||
// Xbox controllers use RX and RY for right stick
|
||||
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");
|
||||
context.isNonStandardDualShock4 = true;
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Detected DualShock 4 (Linux standard mapping)");
|
||||
context.usesLinuxGamepadStandardFaceButtons = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!devName.contains("Xbox") && !devName.contains("XBox") && !devName.contains("X-Box")) {
|
||||
LimeLog.info("Assuming non-standard DualShock 4 mapping on < 4.4");
|
||||
context.isNonStandardDualShock4 = true;
|
||||
}
|
||||
|
||||
if (context.isNonStandardDualShock4) {
|
||||
// The old DS4 driver uses RX and RY for triggers
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||
}
|
||||
else {
|
||||
// If it's not a non-standard DS4 controller, it's probably an Xbox controller or
|
||||
// other sane controller that uses RX and RY for right stick and Z and RZ for triggers.
|
||||
context.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||
context.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||
|
||||
// Xbox controllers use Z and RZ for triggers
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||
context.triggersIdleNegative = true;
|
||||
context.isXboxController = true;
|
||||
// While it's likely that Z and RZ are triggers, we may have digital trigger buttons
|
||||
// instead. We must check that we actually have Z and RZ axes before assigning them.
|
||||
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z) != null &&
|
||||
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ) != null) {
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// DS4 controller uses RX and RY for triggers
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||
context.triggersIdleNegative = true;
|
||||
|
||||
context.isDualShock4 = true;
|
||||
}
|
||||
// Triggers always idle negative on axes that are centered at zero
|
||||
context.triggersIdleNegative = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,7 +593,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) {
|
||||
@@ -565,7 +612,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;
|
||||
@@ -595,7 +642,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
// required fixup is ignoring the select button.
|
||||
else if (devName.equals("Xbox Wireless Controller")) {
|
||||
if (gasRange == null) {
|
||||
context.isXboxBtController = true;
|
||||
context.isNonStandardXboxBtController = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -774,7 +821,58 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
}
|
||||
|
||||
if (context.isDualShock4) {
|
||||
// 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()) {
|
||||
case 304:
|
||||
return KeyEvent.KEYCODE_BUTTON_A;
|
||||
case 305:
|
||||
return KeyEvent.KEYCODE_BUTTON_B;
|
||||
case 307:
|
||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||
case 308:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.isNonStandardDualShock4) {
|
||||
switch (event.getScanCode()) {
|
||||
case 304:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
@@ -819,7 +917,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
}
|
||||
}
|
||||
else if (context.isXboxBtController) {
|
||||
else if (context.isNonStandardXboxBtController) {
|
||||
switch (event.getScanCode()) {
|
||||
case 306:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
@@ -848,19 +946,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
|
||||
@@ -918,6 +1022,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);
|
||||
@@ -991,17 +1110,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1179,6 +1302,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
int keyCode = handleRemapping(context, event);
|
||||
|
||||
if (prefConfig.flipFaceButtons) {
|
||||
keyCode = handleFlipFaceButtons(keyCode);
|
||||
}
|
||||
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -1216,15 +1344,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:
|
||||
@@ -1272,42 +1416,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're emulating the select button
|
||||
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||
{
|
||||
// If either start or LB is up, select comes up too
|
||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
|
||||
{
|
||||
context.inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||
|
||||
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
||||
|
||||
try {
|
||||
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're emulating the special button
|
||||
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
||||
{
|
||||
// If either start or select and RB is up, the special button comes up too
|
||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
||||
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
|
||||
{
|
||||
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
|
||||
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
||||
|
||||
try {
|
||||
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
sendControllerInputPacket(context);
|
||||
|
||||
if (context.pendingExit && context.inputMap == 0) {
|
||||
// All buttons from the quit combo are lifted. Finish the activity now.
|
||||
activityContext.finish();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1318,6 +1433,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
int keyCode = handleRemapping(context, event);
|
||||
|
||||
if (prefConfig.flipFaceButtons) {
|
||||
keyCode = handleFlipFaceButtons(keyCode);
|
||||
}
|
||||
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -1338,15 +1458,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:
|
||||
@@ -1392,30 +1528,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start+LB acts like select for controllers with one button
|
||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||
{
|
||||
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
||||
|
||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
|
||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||
}
|
||||
|
||||
|
||||
// We don't need to send repeat key down events, but the platform
|
||||
// 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.
|
||||
@@ -1486,6 +1605,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());
|
||||
}
|
||||
}
|
||||
@@ -1496,8 +1616,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
usbDeviceContexts.put(controller.getControllerId(), context);
|
||||
}
|
||||
|
||||
class GenericControllerContext {
|
||||
static class GenericControllerContext {
|
||||
public int id;
|
||||
public boolean external;
|
||||
|
||||
public int vendorId;
|
||||
public int productId;
|
||||
|
||||
public float leftStickDeadzoneRadius;
|
||||
public float rightStickDeadzoneRadius;
|
||||
@@ -1518,6 +1642,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 {
|
||||
@@ -1537,17 +1668,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
|
||||
public int hatXAxis = -1;
|
||||
public int hatYAxis = -1;
|
||||
public boolean hatXAxisUsed, hatYAxisUsed;
|
||||
|
||||
public boolean isDualShock4;
|
||||
public boolean isXboxController;
|
||||
public boolean isXboxBtController;
|
||||
public boolean isNonStandardDualShock4;
|
||||
public boolean usesLinuxGamepadStandardFaceButtons;
|
||||
public boolean isNonStandardXboxBtController;
|
||||
public boolean isServal;
|
||||
public boolean backIsStart;
|
||||
public boolean modeIsSelect;
|
||||
public boolean ignoreBack;
|
||||
public boolean hasJoystickAxes;
|
||||
|
||||
public int emulatingButtonFlags = 0;
|
||||
public boolean pendingExit;
|
||||
|
||||
// Used for OUYA bumper state tracking since they force all buttons
|
||||
// up when the OUYA button goes down. We watch the last time we get
|
||||
@@ -1558,9 +1689,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,290 +8,290 @@ import android.view.KeyEvent;
|
||||
* @author Cameron Gutman
|
||||
*/
|
||||
public class KeyboardTranslator {
|
||||
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
*/
|
||||
private static final short KEY_PREFIX = (short) 0x80;
|
||||
|
||||
public static final int VK_0 = 48;
|
||||
public static final int VK_9 = 57;
|
||||
public static final int VK_A = 65;
|
||||
public static final int VK_Z = 90;
|
||||
public static final int VK_NUMPAD0 = 96;
|
||||
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
*/
|
||||
private static final short KEY_PREFIX = (short) 0x80;
|
||||
|
||||
public static final int VK_0 = 48;
|
||||
public static final int VK_9 = 57;
|
||||
public static final int VK_A = 65;
|
||||
public static final int VK_Z = 90;
|
||||
public static final int VK_NUMPAD0 = 96;
|
||||
public static final int VK_BACK_SLASH = 92;
|
||||
public static final int VK_CAPS_LOCK = 20;
|
||||
public static final int VK_CLEAR = 12;
|
||||
public static final int VK_COMMA = 44;
|
||||
public static final int VK_BACK_SPACE = 8;
|
||||
public static final int VK_EQUALS = 61;
|
||||
public static final int VK_ESCAPE = 27;
|
||||
public static final int VK_F1 = 112;
|
||||
public static final int VK_END = 35;
|
||||
public static final int VK_HOME = 36;
|
||||
public static final int VK_NUM_LOCK = 144;
|
||||
public static final int VK_PAGE_UP = 33;
|
||||
public static final int VK_PAGE_DOWN = 34;
|
||||
public static final int VK_PLUS = 521;
|
||||
public static final int VK_CLOSE_BRACKET = 93;
|
||||
public static final int VK_SCROLL_LOCK = 145;
|
||||
public static final int VK_SEMICOLON = 59;
|
||||
public static final int VK_SLASH = 47;
|
||||
public static final int VK_SPACE = 32;
|
||||
public static final int VK_PRINTSCREEN = 154;
|
||||
public static final int VK_TAB = 9;
|
||||
public static final int VK_LEFT = 37;
|
||||
public static final int VK_RIGHT = 39;
|
||||
public static final int VK_UP = 38;
|
||||
public static final int VK_DOWN = 40;
|
||||
public static final int VK_BACK_QUOTE = 192;
|
||||
public static final int VK_QUOTE = 222;
|
||||
public static final int VK_PAUSE = 19;
|
||||
public static final int VK_CLEAR = 12;
|
||||
public static final int VK_COMMA = 44;
|
||||
public static final int VK_BACK_SPACE = 8;
|
||||
public static final int VK_EQUALS = 61;
|
||||
public static final int VK_ESCAPE = 27;
|
||||
public static final int VK_F1 = 112;
|
||||
public static final int VK_END = 35;
|
||||
public static final int VK_HOME = 36;
|
||||
public static final int VK_NUM_LOCK = 144;
|
||||
public static final int VK_PAGE_UP = 33;
|
||||
public static final int VK_PAGE_DOWN = 34;
|
||||
public static final int VK_PLUS = 521;
|
||||
public static final int VK_CLOSE_BRACKET = 93;
|
||||
public static final int VK_SCROLL_LOCK = 145;
|
||||
public static final int VK_SEMICOLON = 59;
|
||||
public static final int VK_SLASH = 47;
|
||||
public static final int VK_SPACE = 32;
|
||||
public static final int VK_PRINTSCREEN = 154;
|
||||
public static final int VK_TAB = 9;
|
||||
public static final int VK_LEFT = 37;
|
||||
public static final int VK_RIGHT = 39;
|
||||
public static final int VK_UP = 38;
|
||||
public static final int VK_DOWN = 40;
|
||||
public static final int VK_BACK_QUOTE = 192;
|
||||
public static final int VK_QUOTE = 222;
|
||||
public static final int VK_PAUSE = 19;
|
||||
|
||||
public static boolean needsShift(int keycode) {
|
||||
switch (keycode)
|
||||
{
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
return true;
|
||||
public static boolean needsShift(int keycode) {
|
||||
switch (keycode)
|
||||
{
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given keycode and returns the GFE keycode
|
||||
* @param keycode the code to be translated
|
||||
* @return a GFE keycode for the given keycode
|
||||
*/
|
||||
public static short translate(int keycode) {
|
||||
int translated;
|
||||
|
||||
// This is a poor man's mapping between Android key codes
|
||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_A &&
|
||||
keycode <= KeyEvent.KEYCODE_Z) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
||||
keycode <= KeyEvent.KEYCODE_F12) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
||||
}
|
||||
else {
|
||||
switch (keycode) {
|
||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||
translated = 0xA4;
|
||||
break;
|
||||
/**
|
||||
* Translates the given keycode and returns the GFE keycode
|
||||
* @param keycode the code to be translated
|
||||
* @return a GFE keycode for the given keycode
|
||||
*/
|
||||
public static short translate(int keycode) {
|
||||
int translated;
|
||||
|
||||
// This is a poor man's mapping between Android key codes
|
||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_A &&
|
||||
keycode <= KeyEvent.KEYCODE_Z) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
||||
keycode <= KeyEvent.KEYCODE_F12) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
||||
}
|
||||
else {
|
||||
switch (keycode) {
|
||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||
translated = 0xA4;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||
translated = 0xA5;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BACKSLASH:
|
||||
translated = 0xdc;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CAPS_LOCK:
|
||||
translated = VK_CAPS_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CLEAR:
|
||||
translated = VK_CLEAR;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_COMMA:
|
||||
translated = 0xbc;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||
translated = 0xA2;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||
translated = 0xA5;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BACKSLASH:
|
||||
translated = 0xdc;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CAPS_LOCK:
|
||||
translated = VK_CAPS_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CLEAR:
|
||||
translated = VK_CLEAR;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_COMMA:
|
||||
translated = 0xbc;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||
translated = 0xA2;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||
translated = 0xA3;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DEL:
|
||||
translated = VK_BACK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
translated = 0x0d;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||
translated = 0xA3;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DEL:
|
||||
translated = VK_BACK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
translated = 0x0d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_EQUALS:
|
||||
translated = 0xbb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ESCAPE:
|
||||
translated = VK_ESCAPE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||
translated = 0x2e;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_INSERT:
|
||||
translated = 0x2d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||
translated = 0xdb;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_EQUALS:
|
||||
translated = 0xbb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ESCAPE:
|
||||
translated = VK_ESCAPE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||
translated = 0x2e;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_INSERT:
|
||||
translated = 0x2d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||
translated = 0xdb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_META_LEFT:
|
||||
translated = 0x5b;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_META_LEFT:
|
||||
translated = 0x5b;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_META_RIGHT:
|
||||
translated = 0x5c;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_META_RIGHT:
|
||||
translated = 0x5c;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MINUS:
|
||||
translated = 0xbd;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MOVE_END:
|
||||
translated = VK_END;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MOVE_HOME:
|
||||
translated = VK_HOME;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUM_LOCK:
|
||||
translated = VK_NUM_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||
translated = VK_PAGE_DOWN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PAGE_UP:
|
||||
translated = VK_PAGE_UP;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PERIOD:
|
||||
translated = 0xbe;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
||||
translated = 0xdd;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
||||
translated = VK_SCROLL_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SEMICOLON:
|
||||
translated = 0xba;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||
translated = 0xA0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_MINUS:
|
||||
translated = 0xbd;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MOVE_END:
|
||||
translated = VK_END;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MOVE_HOME:
|
||||
translated = VK_HOME;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUM_LOCK:
|
||||
translated = VK_NUM_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||
translated = VK_PAGE_DOWN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PAGE_UP:
|
||||
translated = VK_PAGE_UP;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PERIOD:
|
||||
translated = 0xbe;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
||||
translated = 0xdd;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
||||
translated = VK_SCROLL_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SEMICOLON:
|
||||
translated = 0xba;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||
translated = 0xA0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||
translated = 0xA1;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SLASH:
|
||||
translated = 0xbf;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SPACE:
|
||||
translated = VK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SYSRQ:
|
||||
// Android defines this as SysRq/PrntScrn
|
||||
translated = VK_PRINTSCREEN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_TAB:
|
||||
translated = VK_TAB;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
translated = VK_LEFT;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
translated = VK_RIGHT;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
translated = VK_UP;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
translated = VK_DOWN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_GRAVE:
|
||||
translated = VK_BACK_QUOTE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_APOSTROPHE:
|
||||
translated = 0xde;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BREAK:
|
||||
translated = VK_PAUSE;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||
translated = 0xA1;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SLASH:
|
||||
translated = 0xbf;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SPACE:
|
||||
translated = VK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SYSRQ:
|
||||
// Android defines this as SysRq/PrntScrn
|
||||
translated = VK_PRINTSCREEN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_TAB:
|
||||
translated = VK_TAB;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
translated = VK_LEFT;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
translated = VK_RIGHT;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
translated = VK_UP;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
translated = VK_DOWN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_GRAVE:
|
||||
translated = VK_BACK_QUOTE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_APOSTROPHE:
|
||||
translated = 0xde;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BREAK:
|
||||
translated = VK_PAUSE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||
translated = 0x6F;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||
translated = 0x6F;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||
translated = 0x6A;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||
translated = 0x6A;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||
translated = 0x6D;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||
translated = 0x6D;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||
translated = 0x6B;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||
translated = 0x6B;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||
translated = 0x6E;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||
translated = 0x6E;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
translated = 2 + VK_0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
translated = 2 + VK_0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
translated = 3 + VK_0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
translated = 3 + VK_0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
translated = 8 + VK_0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
translated = 8 + VK_0;
|
||||
break;
|
||||
|
||||
default:
|
||||
System.out.println("No key for "+keycode);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (short) ((KEY_PREFIX << 8) | translated);
|
||||
}
|
||||
default:
|
||||
System.out.println("No key for "+keycode);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (short) ((KEY_PREFIX << 8) | translated);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRecognizedInputDevice(UsbDevice device) {
|
||||
public static boolean isRecognizedInputDevice(UsbDevice device) {
|
||||
// On KitKat and later, we can determine if this VID and PID combo
|
||||
// matches an existing input device and defer to the built-in controller
|
||||
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||
@@ -191,10 +191,32 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean kernelSupportsXboxOne() {
|
||||
String kernelVersion = System.getProperty("os.version");
|
||||
LimeLog.info("Kernel Version: "+kernelVersion);
|
||||
|
||||
if (kernelVersion == null) {
|
||||
// We'll assume this is some newer version of Android
|
||||
// that doesn't let you read the kernel version this way.
|
||||
return true;
|
||||
}
|
||||
else if (kernelVersion.startsWith("2.") || kernelVersion.startsWith("3.")) {
|
||||
// These are old kernels that definitely don't support Xbox One controllers properly
|
||||
return false;
|
||||
}
|
||||
else if (kernelVersion.startsWith("4.4.") || kernelVersion.startsWith("4.9.")) {
|
||||
// These aren't guaranteed to have backported kernel patches for proper Xbox One
|
||||
// support (though some devices will).
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// The next AOSP common kernel is 4.14 which has working Xbox One controller support
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
||||
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||
// if we know the kernel isn't already driving this device.
|
||||
return XboxOneController.canClaimDevice(device) ||
|
||||
return ((!kernelSupportsXboxOne() || !isRecognizedInputDevice(device) || claimAllAvailable) && XboxOneController.canClaimDevice(device)) ||
|
||||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -210,7 +210,7 @@ public class AnalogStick extends VirtualControllerElement {
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
// calculate new radius sizes depending
|
||||
radius_complete = getPercent(getCorrectWidth() / 2, 100);
|
||||
radius_complete = getPercent(getCorrectWidth() / 2, 100) - 2 * getDefaultStrokeWidth();
|
||||
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||
|
||||
@@ -293,12 +293,12 @@ public class AnalogStick extends VirtualControllerElement {
|
||||
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||
movement_angle = getAngle(relative_x, relative_y);
|
||||
|
||||
// chop radius if out of outer circle and already pressed
|
||||
// pass touch event to parent if out of outer circle
|
||||
if (movement_radius > radius_complete && !isPressed())
|
||||
return false;
|
||||
|
||||
// chop radius if out of outer circle or near the edge
|
||||
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||
// not pressed already, so ignore event from outer circle
|
||||
if (!isPressed()) {
|
||||
return false;
|
||||
}
|
||||
movement_radius = radius_complete - radius_analog_stick;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
@@ -60,6 +61,7 @@ public class DigitalButton extends VirtualControllerElement {
|
||||
private TimerLongClickTimerTask longClickTimerTask = null;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
private final RectF rect = new RectF();
|
||||
|
||||
private int layer;
|
||||
private DigitalButton movingButton = null;
|
||||
@@ -144,14 +146,18 @@ public class DigitalButton extends VirtualControllerElement {
|
||||
// set transparent background
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
paint.setTextSize(getPercent(getWidth(), 30));
|
||||
paint.setTextSize(getPercent(getWidth(), 25));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
|
||||
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
||||
|
||||
rect.left = rect.top = paint.getStrokeWidth();
|
||||
rect.right = getWidth() - rect.left;
|
||||
rect.bottom = getHeight() - rect.top;
|
||||
|
||||
canvas.drawOval(rect, paint);
|
||||
|
||||
if (icon != -1) {
|
||||
Drawable d = getResources().getDrawable(icon);
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class VirtualController {
|
||||
public class ControllerInputContext {
|
||||
public static class ControllerInputContext {
|
||||
public short inputMap = 0x0000;
|
||||
public byte leftTrigger = 0x00;
|
||||
public byte rightTrigger = 0x00;
|
||||
@@ -34,7 +34,8 @@ public class VirtualController {
|
||||
|
||||
public enum ControllerMode {
|
||||
Active,
|
||||
Configuration
|
||||
MoveButtons,
|
||||
ResizeButtons
|
||||
}
|
||||
|
||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||
@@ -72,13 +73,16 @@ public class VirtualController {
|
||||
public void onClick(View v) {
|
||||
String message;
|
||||
|
||||
if (currentMode == ControllerMode.Configuration) {
|
||||
if (currentMode == ControllerMode.Active){
|
||||
currentMode = ControllerMode.MoveButtons;
|
||||
message = "Entering configuration mode (Move buttons)";
|
||||
} else if (currentMode == ControllerMode.MoveButtons) {
|
||||
currentMode = ControllerMode.ResizeButtons;
|
||||
message = "Entering configuration mode (Resize buttons)";
|
||||
} else {
|
||||
currentMode = ControllerMode.Active;
|
||||
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
||||
message = "Exiting configuration mode";
|
||||
} else {
|
||||
currentMode = ControllerMode.Configuration;
|
||||
message = "Entering configuration mode";
|
||||
}
|
||||
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
@@ -90,6 +94,7 @@ public class VirtualController {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
@@ -120,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);
|
||||
|
||||
@@ -24,6 +24,11 @@ public class VirtualControllerConfigurationLoader {
|
||||
return (int) (((float) total / (float) 100) * (float) percent);
|
||||
}
|
||||
|
||||
// The default controls are specified using a grid of 128*72 cells at 16:9
|
||||
private static int screenScale(int units, int height) {
|
||||
return (int) (((float) height / (float) 72) * (float) units);
|
||||
}
|
||||
|
||||
private static DigitalPad createDigitalPad(
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
@@ -145,149 +150,186 @@ public class VirtualControllerConfigurationLoader {
|
||||
return new RightAnalogStick(controller, context);
|
||||
}
|
||||
|
||||
private static final int BUTTON_BASE_X = 65;
|
||||
private static final int BUTTON_BASE_Y = 5;
|
||||
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
||||
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
||||
|
||||
private static final int TRIGGER_L_BASE_X = 1;
|
||||
private static final int TRIGGER_R_BASE_X = 92;
|
||||
private static final int TRIGGER_DISTANCE = 23;
|
||||
private static final int TRIGGER_BASE_Y = 31;
|
||||
private static final int TRIGGER_WIDTH = 12;
|
||||
private static final int TRIGGER_HEIGHT = 9;
|
||||
|
||||
// Face buttons are defined based on the Y button (button number 9)
|
||||
private static final int BUTTON_BASE_X = 106;
|
||||
private static final int BUTTON_BASE_Y = 1;
|
||||
private static final int BUTTON_SIZE = 10;
|
||||
|
||||
private static final int DPAD_BASE_X = 4;
|
||||
private static final int DPAD_BASE_Y = 41;
|
||||
private static final int DPAD_SIZE = 30;
|
||||
|
||||
private static final int ANALOG_L_BASE_X = 6;
|
||||
private static final int ANALOG_L_BASE_Y = 4;
|
||||
private static final int ANALOG_R_BASE_X = 98;
|
||||
private static final int ANALOG_R_BASE_Y = 42;
|
||||
private static final int ANALOG_SIZE = 26;
|
||||
|
||||
private static final int L3_R3_BASE_Y = 60;
|
||||
|
||||
private static final int START_X = 83;
|
||||
private static final int BACK_X = 34;
|
||||
private static final int START_BACK_Y = 64;
|
||||
private static final int START_BACK_WIDTH = 12;
|
||||
private static final int START_BACK_HEIGHT = 7;
|
||||
|
||||
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||
|
||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
||||
|
||||
// Displace controls on the right by this amount of pixels to account for different aspect ratios
|
||||
int rightDisplacement = screen.widthPixels - screen.heightPixels * 16 / 9;
|
||||
|
||||
int height = screen.heightPixels;
|
||||
|
||||
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
||||
// into a single call. Due to floating point rounding, this isn't actually possible.
|
||||
|
||||
if (!config.onlyL3R3)
|
||||
{
|
||||
controller.addElement(createDigitalPad(controller, context),
|
||||
getPercent(5, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(30, screen.widthPixels),
|
||||
getPercent(40, screen.heightPixels)
|
||||
screenScale(DPAD_BASE_X, height),
|
||||
screenScale(DPAD_BASE_Y, height),
|
||||
screenScale(DPAD_SIZE, height),
|
||||
screenScale(DPAD_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_A,
|
||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
!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),
|
||||
screenScale(BUTTON_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_B,
|
||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
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),
|
||||
screenScale(BUTTON_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_X,
|
||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
!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),
|
||||
screenScale(BUTTON_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_Y,
|
||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
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),
|
||||
screenScale(BUTTON_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createLeftTrigger(
|
||||
0, "LT", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
1, "LT", -1, controller, context),
|
||||
screenScale(TRIGGER_L_BASE_X, height),
|
||||
screenScale(TRIGGER_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createRightTrigger(
|
||||
0, "RT", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
1, "RT", -1, controller, context),
|
||||
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
|
||||
screenScale(TRIGGER_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_LB,
|
||||
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
screenScale(TRIGGER_L_BASE_X + TRIGGER_DISTANCE, height),
|
||||
screenScale(TRIGGER_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_RB,
|
||||
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
screenScale(TRIGGER_R_BASE_X, height) + rightDisplacement,
|
||||
screenScale(TRIGGER_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createLeftStick(controller, context),
|
||||
getPercent(5, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels)
|
||||
screenScale(ANALOG_L_BASE_X, height),
|
||||
screenScale(ANALOG_L_BASE_Y, height),
|
||||
screenScale(ANALOG_SIZE, height),
|
||||
screenScale(ANALOG_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createRightStick(controller, context),
|
||||
getPercent(55, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels)
|
||||
screenScale(ANALOG_R_BASE_X, height) + rightDisplacement,
|
||||
screenScale(ANALOG_R_BASE_Y, height),
|
||||
screenScale(ANALOG_SIZE, height),
|
||||
screenScale(ANALOG_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_BACK,
|
||||
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(90, screen.heightPixels),
|
||||
getPercent(10, screen.widthPixels),
|
||||
getPercent(10, screen.heightPixels)
|
||||
screenScale(BACK_X, height),
|
||||
screenScale(START_BACK_Y, height),
|
||||
screenScale(START_BACK_WIDTH, height),
|
||||
screenScale(START_BACK_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_START,
|
||||
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
|
||||
getPercent(90, screen.heightPixels),
|
||||
getPercent(10, screen.widthPixels),
|
||||
getPercent(10, screen.heightPixels)
|
||||
screenScale(START_X, height) + rightDisplacement,
|
||||
screenScale(START_BACK_Y, height),
|
||||
screenScale(START_BACK_WIDTH, height),
|
||||
screenScale(START_BACK_HEIGHT, height)
|
||||
);
|
||||
}
|
||||
else {
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_LSB,
|
||||
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
||||
getPercent(2, screen.widthPixels),
|
||||
getPercent(80, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
screenScale(TRIGGER_L_BASE_X, height),
|
||||
screenScale(L3_R3_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_RSB,
|
||||
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
||||
getPercent(89, screen.widthPixels),
|
||||
getPercent(80, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
|
||||
screenScale(L3_R3_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
}
|
||||
|
||||
controller.setOpacity(config.oscOpacity);
|
||||
}
|
||||
|
||||
public static void saveProfile(final VirtualController controller,
|
||||
@@ -306,7 +348,7 @@ public class VirtualControllerConfigurationLoader {
|
||||
prefEditor.apply();
|
||||
}
|
||||
|
||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
||||
|
||||
for (VirtualControllerElement element : controller.getElements()) {
|
||||
@@ -324,5 +366,5 @@ public class VirtualControllerConfigurationLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ public abstract class VirtualControllerElement extends View {
|
||||
|
||||
private int normalColor = 0xF0888888;
|
||||
protected int pressedColor = 0xF00000FF;
|
||||
private int configNormalColor = 0xF0FF0000;
|
||||
private int configMoveColor = 0xF0FF0000;
|
||||
private int configResizeColor = 0xF0FF00FF;
|
||||
private int configSelectedColor = 0xF000FF00;
|
||||
|
||||
protected int startSize_x;
|
||||
@@ -112,34 +113,34 @@ public abstract class VirtualControllerElement extends View {
|
||||
|
||||
/*
|
||||
protected void actionShowNormalColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog)
|
||||
{}
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog)
|
||||
{}
|
||||
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
normalColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
normalColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
|
||||
protected void actionShowPressedColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog) {
|
||||
}
|
||||
protected void actionShowPressedColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
pressedColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
pressedColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
*/
|
||||
|
||||
protected void actionEnableMove() {
|
||||
@@ -156,8 +157,12 @@ public abstract class VirtualControllerElement extends View {
|
||||
}
|
||||
|
||||
protected int getDefaultColor() {
|
||||
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
||||
configNormalColor : normalColor;
|
||||
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
|
||||
return configMoveColor;
|
||||
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
|
||||
return configResizeColor;
|
||||
else
|
||||
return normalColor;
|
||||
}
|
||||
|
||||
protected int getDefaultStrokeWidth() {
|
||||
@@ -195,11 +200,11 @@ public abstract class VirtualControllerElement extends View {
|
||||
break;
|
||||
}
|
||||
/*
|
||||
case 2: { // set default color
|
||||
case 2: { // set default color
|
||||
actionShowNormalColorChooser();
|
||||
break;
|
||||
}
|
||||
case 3: { // set pressed color
|
||||
case 3: { // set pressed color
|
||||
actionShowPressedColorChooser();
|
||||
break;
|
||||
}
|
||||
@@ -230,7 +235,10 @@ public abstract class VirtualControllerElement extends View {
|
||||
startSize_x = getWidth();
|
||||
startSize_y = getHeight();
|
||||
|
||||
actionEnableMove();
|
||||
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
|
||||
actionEnableMove();
|
||||
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
|
||||
actionEnableResize();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -287,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());
|
||||
|
||||
@@ -190,8 +194,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
return avcDecoder != null;
|
||||
}
|
||||
|
||||
public boolean is49FpsBlacklisted() {
|
||||
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedFor49Fps(avcDecoder.getName());
|
||||
public boolean isBlacklistedForFrameRate(int frameRate) {
|
||||
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedForFrameRate(avcDecoder.getName(), frameRate);
|
||||
}
|
||||
|
||||
public void enableLegacyFrameDropRendering() {
|
||||
@@ -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,16 +318,39 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Operate at maximum rate to lower latency as much as possible on
|
||||
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
|
||||
// but that will actually result in the decoder crashing if it can't satisfy
|
||||
// our (ludicrous) operating rate requirement.
|
||||
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
|
||||
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);
|
||||
}
|
||||
|
||||
if (MediaCodecHelper.decoderSupportsMaxOperatingRate(selectedDecoderName)) {
|
||||
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 +375,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 +487,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;
|
||||
@@ -683,17 +726,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
// for known resolution combinations. Reference frame invalidation may need
|
||||
// these, so leave them be for those decoders.
|
||||
if (!refFrameInvalidationActive) {
|
||||
if (initialWidth <= 720 && initialHeight <= 480) {
|
||||
if (initialWidth <= 720 && initialHeight <= 480 && refreshRate <= 60) {
|
||||
// Max 5 buffered frames at 720x480x60
|
||||
LimeLog.info("Patching level_idc to 31");
|
||||
sps.levelIdc = 31;
|
||||
}
|
||||
else if (initialWidth <= 1280 && initialHeight <= 720) {
|
||||
else if (initialWidth <= 1280 && initialHeight <= 720 && refreshRate <= 60) {
|
||||
// Max 5 buffered frames at 1280x720x60
|
||||
LimeLog.info("Patching level_idc to 32");
|
||||
sps.levelIdc = 32;
|
||||
}
|
||||
else if (initialWidth <= 1920 && initialHeight <= 1080) {
|
||||
else if (initialWidth <= 1920 && initialHeight <= 1080 && refreshRate <= 60) {
|
||||
// Max 4 buffered frames at 1920x1080x64
|
||||
LimeLog.info("Patching level_idc to 42");
|
||||
sps.levelIdc = 42;
|
||||
@@ -718,12 +761,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
// GFE 2.5.11 changed the SPS to add additional extensions
|
||||
// Some devices don't like these so we remove them here.
|
||||
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||
// Some devices don't like these so we remove them here on old devices.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||
}
|
||||
|
||||
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
|
||||
// Some older devices used to choke on a bitstream restrictions, so we won't provide them
|
||||
// unless explicitly whitelisted. For newer devices, leave the bitstream restrictions present.
|
||||
if (needsSpsBitstreamFixup || isExynos4 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||
|
||||
@@ -1014,18 +1061,39 @@ 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";
|
||||
str += "Foreground: "+renderer.foreground+"\n";
|
||||
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";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
@@ -22,13 +24,19 @@ import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
import com.limelight.utils.NetHelper;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
@@ -54,6 +62,7 @@ public class ComputerManagerService extends Service {
|
||||
private ComputerManagerListener listener = null;
|
||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||
private boolean pollingActive = false;
|
||||
private final Lock defaultNetworkLock = new ReentrantLock();
|
||||
|
||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||
@@ -294,6 +303,66 @@ public class ComputerManagerService extends Service {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void populateExternalAddress(ComputerDetails details) {
|
||||
boolean boundToNetwork = false;
|
||||
boolean activeNetworkIsVpn = NetHelper.isActiveNetworkVpn(this);
|
||||
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
|
||||
// Check if we're currently connected to a VPN which may send our
|
||||
// STUN request from an unexpected interface
|
||||
if (activeNetworkIsVpn) {
|
||||
// Acquire the default network lock since we could be changing global process state
|
||||
defaultNetworkLock.lock();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// On Lollipop or later, we can bind our process to the underlying interface
|
||||
// to ensure our STUN request goes out on that interface or not at all (which is
|
||||
// preferable to getting a VPN endpoint address back).
|
||||
Network[] networks = connMgr.getAllNetworks();
|
||||
for (Network net : networks) {
|
||||
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(net);
|
||||
if (netCaps != null) {
|
||||
if (!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
||||
!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
// This network looks like an underlying multicast-capable transport,
|
||||
// so let's guess that it's probably where our mDNS response came from.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (connMgr.bindProcessToNetwork(net)) {
|
||||
boundToNetwork = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (ConnectivityManager.setProcessDefaultNetwork(net)) {
|
||||
boundToNetwork = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the STUN request if we're not on a VPN or if we bound to a network
|
||||
if (!activeNetworkIsVpn || boundToNetwork) {
|
||||
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||
}
|
||||
|
||||
// Unbind from the network
|
||||
if (boundToNetwork) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
connMgr.bindProcessToNetwork(null);
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
ConnectivityManager.setProcessDefaultNetwork(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock the network state
|
||||
if (activeNetworkIsVpn) {
|
||||
defaultNetworkLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||
return new MdnsDiscoveryListener() {
|
||||
@Override
|
||||
@@ -308,7 +377,7 @@ public class ComputerManagerService extends Service {
|
||||
// our WAN address, which is also very likely the WAN address
|
||||
// of the PC. We can use this later to connect remotely.
|
||||
if (computer.getLocalAddress() instanceof Inet4Address) {
|
||||
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||
populateExternalAddress(details);
|
||||
}
|
||||
}
|
||||
if (computer.getIpv6Address() != null) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
@@ -39,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 {
|
||||
@@ -76,7 +75,8 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||
new NetworkAssetLoader(context, uniqueId),
|
||||
new MemoryAssetLoader(),
|
||||
new DiskAssetLoader(context));
|
||||
new DiskAssetLoader(context),
|
||||
BitmapFactory.decodeResource(context.getResources(), R.drawable.no_app_image));
|
||||
|
||||
// This will trigger the view to reload with the new layout
|
||||
setLayoutId(getLayoutIdForPreferences(prefs));
|
||||
@@ -111,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;
|
||||
|
||||
@@ -52,15 +55,17 @@ public class CachedAppAssetLoader {
|
||||
private final MemoryAssetLoader memoryLoader;
|
||||
private final DiskAssetLoader diskLoader;
|
||||
private final Bitmap placeholderBitmap;
|
||||
private final Bitmap noAppImageBitmap;
|
||||
|
||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||
DiskAssetLoader diskLoader) {
|
||||
DiskAssetLoader diskLoader, Bitmap noAppImageBitmap) {
|
||||
this.computer = computer;
|
||||
this.scalingDivider = scalingDivider;
|
||||
this.networkLoader = networkLoader;
|
||||
this.memoryLoader = memoryLoader;
|
||||
this.diskLoader = diskLoader;
|
||||
this.noAppImageBitmap = noAppImageBitmap;
|
||||
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
@@ -87,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
|
||||
@@ -108,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;
|
||||
}
|
||||
@@ -130,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
|
||||
@@ -181,44 +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
|
||||
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
||||
imageView.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, textView, false);
|
||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,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,
|
||||
@@ -306,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);
|
||||
|
||||
@@ -330,7 +362,7 @@ public class CachedAppAssetLoader {
|
||||
return false;
|
||||
}
|
||||
|
||||
public class LoaderTuple {
|
||||
public static class LoaderTuple {
|
||||
public final ComputerDetails computer;
|
||||
public final NvApp app;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,19 @@ import java.security.cert.X509Certificate;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class ConnectionContext {
|
||||
public String serverAddress;
|
||||
public X509Certificate serverCert;
|
||||
public StreamConfiguration streamConfig;
|
||||
public NvConnectionListener connListener;
|
||||
public SecretKey riKey;
|
||||
public int riKeyId;
|
||||
|
||||
// This is the version quad from the appversion tag of /serverinfo
|
||||
public String serverAppVersion;
|
||||
public String serverGfeVersion;
|
||||
|
||||
public int negotiatedWidth, negotiatedHeight;
|
||||
public int negotiatedFps;
|
||||
public boolean negotiatedHdr;
|
||||
public String serverAddress;
|
||||
public X509Certificate serverCert;
|
||||
public StreamConfiguration streamConfig;
|
||||
public NvConnectionListener connListener;
|
||||
public SecretKey riKey;
|
||||
public int riKeyId;
|
||||
|
||||
// This is the version quad from the appversion tag of /serverinfo
|
||||
public String serverAppVersion;
|
||||
public String serverGfeVersion;
|
||||
|
||||
public int negotiatedWidth, negotiatedHeight;
|
||||
public boolean negotiatedHdr;
|
||||
|
||||
public int videoCapabilities;
|
||||
}
|
||||
|
||||
@@ -26,320 +26,337 @@ import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class NvConnection {
|
||||
// Context parameters
|
||||
private String host;
|
||||
private LimelightCryptoProvider cryptoProvider;
|
||||
private String uniqueId;
|
||||
private ConnectionContext context;
|
||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||
private final boolean isMonkey;
|
||||
|
||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
||||
{
|
||||
this.host = host;
|
||||
this.cryptoProvider = cryptoProvider;
|
||||
this.uniqueId = uniqueId;
|
||||
// Context parameters
|
||||
private String host;
|
||||
private LimelightCryptoProvider cryptoProvider;
|
||||
private String uniqueId;
|
||||
private ConnectionContext context;
|
||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||
private final boolean isMonkey;
|
||||
|
||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
||||
{
|
||||
this.host = host;
|
||||
this.cryptoProvider = cryptoProvider;
|
||||
this.uniqueId = uniqueId;
|
||||
|
||||
this.context = new ConnectionContext();
|
||||
this.context.streamConfig = config;
|
||||
this.context.serverCert = serverCert;
|
||||
try {
|
||||
// This is unique per connection
|
||||
this.context.riKey = generateRiAesKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
this.context.riKeyId = generateRiKeyId();
|
||||
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||
}
|
||||
|
||||
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||
|
||||
// RI keys are 128 bits
|
||||
keyGen.init(128);
|
||||
|
||||
return keyGen.generateKey();
|
||||
}
|
||||
|
||||
private static int generateRiKeyId() {
|
||||
return new SecureRandom().nextInt();
|
||||
}
|
||||
this.context = new ConnectionContext();
|
||||
this.context.streamConfig = config;
|
||||
this.context.serverCert = serverCert;
|
||||
|
||||
public void stop() {
|
||||
// Interrupt any pending connection. This is thread-safe.
|
||||
MoonBridge.interruptConnection();
|
||||
// This is unique per connection
|
||||
this.context.riKey = generateRiAesKey();
|
||||
context.riKeyId = generateRiKeyId();
|
||||
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.stopConnection();
|
||||
MoonBridge.cleanupBridge();
|
||||
}
|
||||
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||
}
|
||||
|
||||
private static SecretKey generateRiAesKey() {
|
||||
try {
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||
|
||||
// Now a pending connection can be processed
|
||||
connectionAllowed.release();
|
||||
}
|
||||
|
||||
private boolean startApp() throws XmlPullParserException, IOException
|
||||
{
|
||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||
// RI keys are 128 bits
|
||||
keyGen.init(128);
|
||||
|
||||
String serverInfo = h.getServerInfo();
|
||||
|
||||
context.serverAppVersion = h.getServerVersion(serverInfo);
|
||||
if (context.serverAppVersion == null) {
|
||||
context.connListener.displayMessage("Server version malformed");
|
||||
return false;
|
||||
}
|
||||
return keyGen.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static int generateRiKeyId() {
|
||||
return new SecureRandom().nextInt();
|
||||
}
|
||||
|
||||
// May be missing for older servers
|
||||
context.serverGfeVersion = h.getGfeVersion(serverInfo);
|
||||
|
||||
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
||||
context.connListener.displayMessage("Device not paired with computer");
|
||||
return false;
|
||||
}
|
||||
public void stop() {
|
||||
// Interrupt any pending connection. This is thread-safe.
|
||||
MoonBridge.interruptConnection();
|
||||
|
||||
context.negotiatedHdr = context.streamConfig.getEnableHdr();
|
||||
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
|
||||
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
|
||||
context.negotiatedHdr = false;
|
||||
}
|
||||
|
||||
//
|
||||
// Decide on negotiated stream parameters now
|
||||
//
|
||||
|
||||
// Check for a supported stream resolution
|
||||
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
||||
// Client wants 4K but the server can't do it
|
||||
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
||||
|
||||
// Lower resolution to 1080p
|
||||
context.negotiatedWidth = 1920;
|
||||
context.negotiatedHeight = 1080;
|
||||
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
||||
}
|
||||
else {
|
||||
// Take what the client wanted
|
||||
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
||||
}
|
||||
|
||||
//
|
||||
// Video stream format will be decided during the RTSP handshake
|
||||
//
|
||||
|
||||
NvApp app = context.streamConfig.getApp();
|
||||
|
||||
// If the client did not provide an exact app ID, do a lookup with the applist
|
||||
if (!context.streamConfig.getApp().isInitialized()) {
|
||||
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
|
||||
app = h.getAppByName(context.streamConfig.getApp().getAppName());
|
||||
if (app == null) {
|
||||
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a game running, resume it
|
||||
if (h.getCurrentGame(serverInfo) != 0) {
|
||||
try {
|
||||
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
|
||||
if (!h.resumeApp(context)) {
|
||||
context.connListener.displayMessage("Failed to resume existing session");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return quitAndLaunch(h, context);
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 470) {
|
||||
// This is the error you get when you try to resume a session that's not yours.
|
||||
// Because this is fairly common, we'll display a more detailed message.
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be resumed. End streaming on the original " +
|
||||
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else if (e.getErrorCode() == 525) {
|
||||
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
|
||||
"quit the session and start streaming again.");
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Resumed existing game session");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
}
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.stopConnection();
|
||||
MoonBridge.cleanupBridge();
|
||||
}
|
||||
|
||||
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
|
||||
XmlPullParserException {
|
||||
try {
|
||||
if (!h.quitApp()) {
|
||||
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
|
||||
return false;
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 599) {
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be quit. End streaming on the original " +
|
||||
"device or the PC itself. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
// Now a pending connection can be processed
|
||||
connectionAllowed.release();
|
||||
}
|
||||
|
||||
private boolean startApp() throws XmlPullParserException, IOException
|
||||
{
|
||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
|
||||
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
|
||||
throws IOException, XmlPullParserException {
|
||||
// Launch the app since it's not running
|
||||
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
|
||||
context.connListener.displayMessage("Failed to launch application");
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Launched new game session");
|
||||
|
||||
return true;
|
||||
}
|
||||
String serverInfo = h.getServerInfo();
|
||||
|
||||
context.serverAppVersion = h.getServerVersion(serverInfo);
|
||||
if (context.serverAppVersion == null) {
|
||||
context.connListener.displayMessage("Server version malformed");
|
||||
return false;
|
||||
}
|
||||
|
||||
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
context.connListener = connectionListener;
|
||||
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
|
||||
// May be missing for older servers
|
||||
context.serverGfeVersion = h.getGfeVersion(serverInfo);
|
||||
|
||||
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
||||
context.connListener.displayMessage("Device not paired with computer");
|
||||
return false;
|
||||
}
|
||||
|
||||
String appName = context.streamConfig.getApp().getAppName();
|
||||
context.negotiatedHdr = context.streamConfig.getEnableHdr();
|
||||
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
|
||||
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
|
||||
context.negotiatedHdr = false;
|
||||
}
|
||||
|
||||
//
|
||||
// Decide on negotiated stream parameters now
|
||||
//
|
||||
|
||||
// Check for a supported stream resolution
|
||||
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
||||
// Client wants 4K but the server can't do it
|
||||
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
||||
|
||||
// Lower resolution to 1080p
|
||||
context.negotiatedWidth = 1920;
|
||||
context.negotiatedHeight = 1080;
|
||||
}
|
||||
else {
|
||||
// Take what the client wanted
|
||||
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||
}
|
||||
|
||||
//
|
||||
// Video stream format will be decided during the RTSP handshake
|
||||
//
|
||||
|
||||
NvApp app = context.streamConfig.getApp();
|
||||
|
||||
// If the client did not provide an exact app ID, do a lookup with the applist
|
||||
if (!context.streamConfig.getApp().isInitialized()) {
|
||||
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
|
||||
app = h.getAppByName(context.streamConfig.getApp().getAppName());
|
||||
if (app == null) {
|
||||
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a game running, resume it
|
||||
if (h.getCurrentGame(serverInfo) != 0) {
|
||||
try {
|
||||
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
|
||||
if (!h.resumeApp(context)) {
|
||||
context.connListener.displayMessage("Failed to resume existing session");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return quitAndLaunch(h, context);
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 470) {
|
||||
// This is the error you get when you try to resume a session that's not yours.
|
||||
// Because this is fairly common, we'll display a more detailed message.
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be resumed. End streaming on the original " +
|
||||
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else if (e.getErrorCode() == 525) {
|
||||
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
|
||||
"quit the session and start streaming again.");
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Resumed existing game session");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
}
|
||||
|
||||
context.serverAddress = host;
|
||||
context.connListener.stageStarting(appName);
|
||||
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
|
||||
XmlPullParserException {
|
||||
try {
|
||||
if (!h.quitApp()) {
|
||||
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
|
||||
return false;
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 599) {
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be quit. End streaming on the original " +
|
||||
"device or the PC itself. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!startApp()) {
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
context.connListener.stageComplete(appName);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
|
||||
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
|
||||
throws IOException, XmlPullParserException {
|
||||
// Launch the app since it's not running
|
||||
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
|
||||
context.connListener.displayMessage("Failed to launch application");
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Launched new game session");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ByteBuffer ib = ByteBuffer.allocate(16);
|
||||
ib.putInt(context.riKeyId);
|
||||
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
context.connListener = connectionListener;
|
||||
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
|
||||
|
||||
// Acquire the connection semaphore to ensure we only have one
|
||||
// connection going at once.
|
||||
try {
|
||||
connectionAllowed.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
String appName = context.streamConfig.getApp().getAppName();
|
||||
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||
context.serverAppVersion, context.serverGfeVersion,
|
||||
context.negotiatedWidth, context.negotiatedHeight,
|
||||
context.negotiatedFps, context.streamConfig.getBitrate(),
|
||||
context.streamConfig.getMaxPacketSize(),
|
||||
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
|
||||
context.streamConfig.getHevcSupported(),
|
||||
context.negotiatedHdr,
|
||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||
context.streamConfig.getClientRefreshRateX100(),
|
||||
context.riKey.getEncoded(), ib.array(),
|
||||
context.videoCapabilities);
|
||||
if (ret != 0) {
|
||||
// LiStartConnection() failed, so the caller is not expected
|
||||
// to stop the connection themselves. We need to release their
|
||||
// semaphore count for them.
|
||||
connectionAllowed.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseMove(deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonDown(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonUp(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short controllerNumber,
|
||||
final short activeGamepadMask, final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
|
||||
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
|
||||
leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseScroll(final byte scrollClicks) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseScroll(scrollClicks);
|
||||
}
|
||||
}
|
||||
context.serverAddress = host;
|
||||
context.connListener.stageStarting(appName);
|
||||
|
||||
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
||||
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
||||
}
|
||||
try {
|
||||
if (!startApp()) {
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
context.connListener.stageComplete(appName);
|
||||
} catch (GfeHttpResponseException e) {
|
||||
e.printStackTrace();
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, e.getErrorCode());
|
||||
return;
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer ib = ByteBuffer.allocate(16);
|
||||
ib.putInt(context.riKeyId);
|
||||
|
||||
// Acquire the connection semaphore to ensure we only have one
|
||||
// connection going at once.
|
||||
try {
|
||||
connectionAllowed.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||
context.serverAppVersion, context.serverGfeVersion,
|
||||
context.negotiatedWidth, context.negotiatedHeight,
|
||||
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
||||
context.streamConfig.getMaxPacketSize(),
|
||||
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration().toInt(),
|
||||
context.streamConfig.getHevcSupported(),
|
||||
context.negotiatedHdr,
|
||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||
context.streamConfig.getClientRefreshRateX100(),
|
||||
context.riKey.getEncoded(), ib.array(),
|
||||
context.videoCapabilities);
|
||||
if (ret != 0) {
|
||||
// LiStartConnection() failed, so the caller is not expected
|
||||
// to stop the connection themselves. We need to release their
|
||||
// semaphore count for them.
|
||||
connectionAllowed.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseMove(deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonDown(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonUp(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short controllerNumber,
|
||||
final short activeGamepadMask, final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
|
||||
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
|
||||
leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseScroll(final byte scrollClicks) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseScroll(scrollClicks);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseHighResScroll(final short scrollAmount) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseHighResScroll(scrollAmount);
|
||||
}
|
||||
}
|
||||
|
||||
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
||||
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
public interface NvConnectionListener {
|
||||
void stageStarting(String stage);
|
||||
void stageComplete(String stage);
|
||||
void stageFailed(String stage, long errorCode);
|
||||
|
||||
void connectionStarted();
|
||||
void connectionTerminated(long errorCode);
|
||||
void connectionStatusUpdate(int connectionStatus);
|
||||
|
||||
void displayMessage(String message);
|
||||
void displayTransientMessage(String message);
|
||||
void stageStarting(String stage);
|
||||
void stageComplete(String stage);
|
||||
void stageFailed(String stage, int errorCode);
|
||||
|
||||
void connectionStarted();
|
||||
void connectionTerminated(int errorCode);
|
||||
void connectionStatusUpdate(int connectionStatus);
|
||||
|
||||
void displayMessage(String message);
|
||||
void displayTransientMessage(String message);
|
||||
|
||||
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||
}
|
||||
|
||||
@@ -4,230 +4,211 @@ import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class StreamConfiguration {
|
||||
public static final int INVALID_APP_ID = 0;
|
||||
public static final int INVALID_APP_ID = 0;
|
||||
|
||||
public static final int STREAM_CFG_LOCAL = 0;
|
||||
public static final int STREAM_CFG_REMOTE = 1;
|
||||
public static final int STREAM_CFG_AUTO = 2;
|
||||
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 NvApp app;
|
||||
private int width, height;
|
||||
private int refreshRate;
|
||||
private int launchRefreshRate;
|
||||
private int clientRefreshRateX100;
|
||||
private int bitrate;
|
||||
private boolean sops;
|
||||
private boolean enableAdaptiveResolution;
|
||||
private boolean playLocalAudio;
|
||||
private int maxPacketSize;
|
||||
private int remote;
|
||||
private MoonBridge.AudioConfiguration audioConfiguration;
|
||||
private boolean supportsHevc;
|
||||
private int hevcBitratePercentageMultiplier;
|
||||
private boolean enableHdr;
|
||||
private int attachedGamepadMask;
|
||||
|
||||
private static final int CHANNEL_COUNT_STEREO = 2;
|
||||
private static final int CHANNEL_COUNT_5_1 = 6;
|
||||
|
||||
private static final int CHANNEL_MASK_STEREO = 0x3;
|
||||
private static final int CHANNEL_MASK_5_1 = 0xFC;
|
||||
|
||||
private NvApp app;
|
||||
private int width, height;
|
||||
private int refreshRate;
|
||||
private int clientRefreshRateX100;
|
||||
private int bitrate;
|
||||
private boolean sops;
|
||||
private boolean enableAdaptiveResolution;
|
||||
private boolean playLocalAudio;
|
||||
private int maxPacketSize;
|
||||
private int remote;
|
||||
private int audioChannelMask;
|
||||
private int audioChannelCount;
|
||||
private int audioConfiguration;
|
||||
private boolean supportsHevc;
|
||||
private int hevcBitratePercentageMultiplier;
|
||||
private boolean enableHdr;
|
||||
private int attachedGamepadMask;
|
||||
public static class Builder {
|
||||
private StreamConfiguration config = new StreamConfiguration();
|
||||
|
||||
public StreamConfiguration.Builder setApp(NvApp app) {
|
||||
config.app = app;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
|
||||
config.remote = remote;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setResolution(int width, int height) {
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
|
||||
config.refreshRate = refreshRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private StreamConfiguration config = new StreamConfiguration();
|
||||
|
||||
public StreamConfiguration.Builder setApp(NvApp app) {
|
||||
config.app = app;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
|
||||
config.remote = remote;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setResolution(int width, int height) {
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
|
||||
config.refreshRate = refreshRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setBitrate(int bitrate) {
|
||||
config.bitrate = bitrate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setEnableSops(boolean enable) {
|
||||
config.sops = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
|
||||
config.enableAdaptiveResolution = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
|
||||
config.playLocalAudio = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
|
||||
config.maxPacketSize = maxPacketSize;
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setLaunchRefreshRate(int refreshRate) {
|
||||
config.launchRefreshRate = refreshRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setBitrate(int bitrate) {
|
||||
config.bitrate = bitrate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setEnableSops(boolean enable) {
|
||||
config.sops = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
|
||||
config.enableAdaptiveResolution = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
|
||||
config.playLocalAudio = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
|
||||
config.maxPacketSize = maxPacketSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
|
||||
config.hevcBitratePercentageMultiplier = multiplier;
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
|
||||
config.hevcBitratePercentageMultiplier = multiplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
|
||||
config.enableHdr = enableHdr;
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
|
||||
config.enableHdr = enableHdr;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
|
||||
config.attachedGamepadMask = attachedGamepadMask;
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
|
||||
config.attachedGamepadMask = attachedGamepadMask;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
|
||||
config.attachedGamepadMask = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (gamepadCount > i) {
|
||||
config.attachedGamepadMask |= 1 << i;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
|
||||
config.attachedGamepadMask = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (gamepadCount > i) {
|
||||
config.attachedGamepadMask |= 1 << i;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
|
||||
config.clientRefreshRateX100 = refreshRateX100;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAudioConfiguration(int audioConfig) {
|
||||
if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) {
|
||||
config.audioChannelCount = CHANNEL_COUNT_STEREO;
|
||||
config.audioChannelMask = CHANNEL_MASK_STEREO;
|
||||
}
|
||||
else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) {
|
||||
config.audioChannelCount = CHANNEL_COUNT_5_1;
|
||||
config.audioChannelMask = CHANNEL_MASK_5_1;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Invalid audio configuration");
|
||||
}
|
||||
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
|
||||
config.clientRefreshRateX100 = refreshRateX100;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
|
||||
config.audioConfiguration = audioConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
|
||||
config.supportsHevc = supportsHevc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration build() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
private StreamConfiguration() {
|
||||
// Set default attributes
|
||||
this.app = new NvApp("Steam");
|
||||
this.width = 1280;
|
||||
this.height = 720;
|
||||
this.refreshRate = 60;
|
||||
this.launchRefreshRate = 60;
|
||||
this.bitrate = 10000;
|
||||
this.maxPacketSize = 1024;
|
||||
this.remote = STREAM_CFG_AUTO;
|
||||
this.sops = true;
|
||||
this.enableAdaptiveResolution = false;
|
||||
this.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
|
||||
this.supportsHevc = false;
|
||||
this.enableHdr = false;
|
||||
this.attachedGamepadMask = 0;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getRefreshRate() {
|
||||
return refreshRate;
|
||||
}
|
||||
|
||||
config.audioConfiguration = audioConfig;
|
||||
public int getLaunchRefreshRate() {
|
||||
return launchRefreshRate;
|
||||
}
|
||||
|
||||
public int getBitrate() {
|
||||
return bitrate;
|
||||
}
|
||||
|
||||
public int getMaxPacketSize() {
|
||||
return maxPacketSize;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
|
||||
config.supportsHevc = supportsHevc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration build() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
private StreamConfiguration() {
|
||||
// Set default attributes
|
||||
this.app = new NvApp("Steam");
|
||||
this.width = 1280;
|
||||
this.height = 720;
|
||||
this.refreshRate = 60;
|
||||
this.bitrate = 10000;
|
||||
this.maxPacketSize = 1024;
|
||||
this.remote = STREAM_CFG_AUTO;
|
||||
this.sops = true;
|
||||
this.enableAdaptiveResolution = false;
|
||||
this.audioChannelCount = CHANNEL_COUNT_STEREO;
|
||||
this.audioChannelMask = CHANNEL_MASK_STEREO;
|
||||
this.supportsHevc = false;
|
||||
this.enableHdr = false;
|
||||
this.attachedGamepadMask = 0;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getRefreshRate() {
|
||||
return refreshRate;
|
||||
}
|
||||
|
||||
public int getBitrate() {
|
||||
return bitrate;
|
||||
}
|
||||
|
||||
public int getMaxPacketSize() {
|
||||
return maxPacketSize;
|
||||
}
|
||||
public NvApp getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
public boolean getSops() {
|
||||
return sops;
|
||||
}
|
||||
|
||||
public boolean getAdaptiveResolutionEnabled() {
|
||||
return enableAdaptiveResolution;
|
||||
}
|
||||
|
||||
public boolean getPlayLocalAudio() {
|
||||
return playLocalAudio;
|
||||
}
|
||||
|
||||
public int getRemote() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
public NvApp getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
public boolean getSops() {
|
||||
return sops;
|
||||
}
|
||||
|
||||
public boolean getAdaptiveResolutionEnabled() {
|
||||
return enableAdaptiveResolution;
|
||||
}
|
||||
|
||||
public boolean getPlayLocalAudio() {
|
||||
return playLocalAudio;
|
||||
}
|
||||
|
||||
public int getRemote() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
public int getAudioChannelCount() {
|
||||
return audioChannelCount;
|
||||
}
|
||||
|
||||
public int getAudioChannelMask() {
|
||||
return audioChannelMask;
|
||||
}
|
||||
public MoonBridge.AudioConfiguration getAudioConfiguration() {
|
||||
return audioConfiguration;
|
||||
}
|
||||
|
||||
public boolean getHevcSupported() {
|
||||
return supportsHevc;
|
||||
}
|
||||
|
||||
public int getAudioConfiguration() {
|
||||
return audioConfiguration;
|
||||
}
|
||||
|
||||
public boolean getHevcSupported() {
|
||||
return supportsHevc;
|
||||
}
|
||||
public int getHevcBitratePercentageMultiplier() {
|
||||
return hevcBitratePercentageMultiplier;
|
||||
}
|
||||
|
||||
public int getHevcBitratePercentageMultiplier() {
|
||||
return hevcBitratePercentageMultiplier;
|
||||
}
|
||||
public boolean getEnableHdr() {
|
||||
return enableHdr;
|
||||
}
|
||||
|
||||
public boolean getEnableHdr() {
|
||||
return enableHdr;
|
||||
}
|
||||
public int getAttachedGamepadMask() {
|
||||
return attachedGamepadMask;
|
||||
}
|
||||
|
||||
public int getAttachedGamepadMask() {
|
||||
return attachedGamepadMask;
|
||||
}
|
||||
|
||||
public int getClientRefreshRateX100() {
|
||||
return clientRefreshRateX100;
|
||||
}
|
||||
public int getClientRefreshRateX100() {
|
||||
return clientRefreshRateX100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
public class ByteBufferDescriptor {
|
||||
public byte[] data;
|
||||
public int offset;
|
||||
public int length;
|
||||
|
||||
public ByteBufferDescriptor nextDescriptor;
|
||||
|
||||
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
||||
{
|
||||
this.data = desc.data;
|
||||
this.offset = desc.offset;
|
||||
this.length = desc.length;
|
||||
}
|
||||
|
||||
public void reinitialize(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
this.nextDescriptor = null;
|
||||
}
|
||||
|
||||
public void print()
|
||||
{
|
||||
print(offset, length);
|
||||
}
|
||||
|
||||
public void print(int length)
|
||||
{
|
||||
print(this.offset, length);
|
||||
}
|
||||
|
||||
public void print(int offset, int length)
|
||||
{
|
||||
for (int i = offset; i < offset+length;) {
|
||||
if (i + 8 <= offset+length) {
|
||||
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
|
||||
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
|
||||
i += 8;
|
||||
}
|
||||
else {
|
||||
System.out.printf("%x: %02x \n", i, data[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
public byte[] data;
|
||||
public int offset;
|
||||
public int length;
|
||||
|
||||
public ByteBufferDescriptor nextDescriptor;
|
||||
|
||||
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
||||
{
|
||||
this.data = desc.data;
|
||||
this.offset = desc.offset;
|
||||
this.length = desc.length;
|
||||
}
|
||||
|
||||
public void reinitialize(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
this.nextDescriptor = null;
|
||||
}
|
||||
|
||||
public void print()
|
||||
{
|
||||
print(offset, length);
|
||||
}
|
||||
|
||||
public void print(int length)
|
||||
{
|
||||
print(this.offset, length);
|
||||
}
|
||||
|
||||
public void print(int offset, int length)
|
||||
{
|
||||
for (int i = offset; i < offset+length;) {
|
||||
if (i + 8 <= offset+length) {
|
||||
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
|
||||
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
|
||||
i += 8;
|
||||
}
|
||||
else {
|
||||
System.out.printf("%x: %02x \n", i, data[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package com.limelight.nvstream.av.audio;
|
||||
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public interface AudioRenderer {
|
||||
int setup(int audioConfiguration);
|
||||
int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame);
|
||||
|
||||
void start();
|
||||
void start();
|
||||
|
||||
void stop();
|
||||
|
||||
void playDecodedAudio(short[] audioData);
|
||||
|
||||
void cleanup();
|
||||
void stop();
|
||||
|
||||
void playDecodedAudio(short[] audioData);
|
||||
|
||||
void cleanup();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.limelight.nvstream.av.video;
|
||||
|
||||
public abstract class VideoDecoderRenderer {
|
||||
public abstract int setup(int format, int width, int height, int redrawRate);
|
||||
public abstract int setup(int format, int width, int height, int redrawRate);
|
||||
|
||||
public abstract void start();
|
||||
public abstract void start();
|
||||
|
||||
public abstract void stop();
|
||||
public abstract void stop();
|
||||
|
||||
// This is called once for each frame-start NALU. This means it will be called several times
|
||||
// for an IDR frame which contains several parameter sets and the I-frame data.
|
||||
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||
int frameNumber, long receiveTimeMs);
|
||||
|
||||
public abstract void cleanup();
|
||||
// This is called once for each frame-start NALU. This means it will be called several times
|
||||
// for an IDR frame which contains several parameter sets and the I-frame data.
|
||||
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||
int frameNumber, long receiveTimeMs);
|
||||
|
||||
public abstract void cleanup();
|
||||
|
||||
public abstract int getCapabilities();
|
||||
public abstract int getCapabilities();
|
||||
}
|
||||
|
||||
@@ -4,82 +4,82 @@ import java.security.cert.X509Certificate;
|
||||
|
||||
|
||||
public class ComputerDetails {
|
||||
public enum State {
|
||||
ONLINE, OFFLINE, UNKNOWN
|
||||
}
|
||||
public enum State {
|
||||
ONLINE, OFFLINE, UNKNOWN
|
||||
}
|
||||
|
||||
// Persistent attributes
|
||||
public String uuid;
|
||||
public String name;
|
||||
public String localAddress;
|
||||
public String remoteAddress;
|
||||
public String manualAddress;
|
||||
public String ipv6Address;
|
||||
public String macAddress;
|
||||
public X509Certificate serverCert;
|
||||
// Persistent attributes
|
||||
public String uuid;
|
||||
public String name;
|
||||
public String localAddress;
|
||||
public String remoteAddress;
|
||||
public String manualAddress;
|
||||
public String ipv6Address;
|
||||
public String macAddress;
|
||||
public X509Certificate serverCert;
|
||||
|
||||
// Transient attributes
|
||||
public State state;
|
||||
public String activeAddress;
|
||||
public PairingManager.PairState pairState;
|
||||
public int runningGameId;
|
||||
public String rawAppList;
|
||||
// Transient attributes
|
||||
public State state;
|
||||
public String activeAddress;
|
||||
public PairingManager.PairState pairState;
|
||||
public int runningGameId;
|
||||
public String rawAppList;
|
||||
|
||||
public ComputerDetails() {
|
||||
// Use defaults
|
||||
state = State.UNKNOWN;
|
||||
}
|
||||
public ComputerDetails() {
|
||||
// Use defaults
|
||||
state = State.UNKNOWN;
|
||||
}
|
||||
|
||||
public ComputerDetails(ComputerDetails details) {
|
||||
// Copy details from the other computer
|
||||
update(details);
|
||||
}
|
||||
public ComputerDetails(ComputerDetails details) {
|
||||
// Copy details from the other computer
|
||||
update(details);
|
||||
}
|
||||
|
||||
public void update(ComputerDetails details) {
|
||||
this.state = details.state;
|
||||
this.name = details.name;
|
||||
this.uuid = details.uuid;
|
||||
if (details.activeAddress != null) {
|
||||
this.activeAddress = details.activeAddress;
|
||||
}
|
||||
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
||||
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
||||
this.localAddress = details.localAddress;
|
||||
}
|
||||
if (details.remoteAddress != null) {
|
||||
this.remoteAddress = details.remoteAddress;
|
||||
}
|
||||
if (details.manualAddress != null) {
|
||||
this.manualAddress = details.manualAddress;
|
||||
}
|
||||
if (details.ipv6Address != null) {
|
||||
this.ipv6Address = details.ipv6Address;
|
||||
}
|
||||
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
|
||||
this.macAddress = details.macAddress;
|
||||
}
|
||||
if (details.serverCert != null) {
|
||||
this.serverCert = details.serverCert;
|
||||
}
|
||||
this.pairState = details.pairState;
|
||||
this.runningGameId = details.runningGameId;
|
||||
this.rawAppList = details.rawAppList;
|
||||
}
|
||||
public void update(ComputerDetails details) {
|
||||
this.state = details.state;
|
||||
this.name = details.name;
|
||||
this.uuid = details.uuid;
|
||||
if (details.activeAddress != null) {
|
||||
this.activeAddress = details.activeAddress;
|
||||
}
|
||||
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
||||
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
||||
this.localAddress = details.localAddress;
|
||||
}
|
||||
if (details.remoteAddress != null) {
|
||||
this.remoteAddress = details.remoteAddress;
|
||||
}
|
||||
if (details.manualAddress != null) {
|
||||
this.manualAddress = details.manualAddress;
|
||||
}
|
||||
if (details.ipv6Address != null) {
|
||||
this.ipv6Address = details.ipv6Address;
|
||||
}
|
||||
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
|
||||
this.macAddress = details.macAddress;
|
||||
}
|
||||
if (details.serverCert != null) {
|
||||
this.serverCert = details.serverCert;
|
||||
}
|
||||
this.pairState = details.pairState;
|
||||
this.runningGameId = details.runningGameId;
|
||||
this.rawAppList = details.rawAppList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("State: ").append(state).append("\n");
|
||||
str.append("Active Address: ").append(activeAddress).append("\n");
|
||||
str.append("Name: ").append(name).append("\n");
|
||||
str.append("UUID: ").append(uuid).append("\n");
|
||||
str.append("Local Address: ").append(localAddress).append("\n");
|
||||
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
||||
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
|
||||
str.append("Manual Address: ").append(manualAddress).append("\n");
|
||||
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||
str.append("Pair State: ").append(pairState).append("\n");
|
||||
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||
return str.toString();
|
||||
}
|
||||
@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("UUID: ").append(uuid).append("\n");
|
||||
str.append("Local Address: ").append(localAddress).append("\n");
|
||||
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
||||
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
|
||||
str.append("Manual Address: ").append(manualAddress).append("\n");
|
||||
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||
str.append("Pair State: ").append(pairState).append("\n");
|
||||
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,26 @@ package com.limelight.nvstream.http;
|
||||
import java.io.IOException;
|
||||
|
||||
public class GfeHttpResponseException extends IOException {
|
||||
private static final long serialVersionUID = 1543508830807804222L;
|
||||
|
||||
private int errorCode;
|
||||
private String errorMsg;
|
||||
|
||||
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
||||
this.errorCode = errorCode;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
|
||||
}
|
||||
private static final long serialVersionUID = 1543508830807804222L;
|
||||
|
||||
private int errorCode;
|
||||
private String errorMsg;
|
||||
|
||||
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
||||
this.errorCode = errorCode;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
|
||||
public interface LimelightCryptoProvider {
|
||||
X509Certificate getClientCertificate();
|
||||
RSAPrivateKey getClientPrivateKey();
|
||||
byte[] getPemEncodedClientCertificate();
|
||||
String encodeBase64String(byte[] data);
|
||||
X509Certificate getClientCertificate();
|
||||
RSAPrivateKey getClientPrivateKey();
|
||||
byte[] getPemEncodedClientCertificate();
|
||||
String encodeBase64String(byte[] data);
|
||||
}
|
||||
|
||||
@@ -3,59 +3,68 @@ package com.limelight.nvstream.http;
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class NvApp {
|
||||
private String appName = "";
|
||||
private int appId;
|
||||
private boolean initialized;
|
||||
private boolean hdrSupported;
|
||||
|
||||
public NvApp() {}
|
||||
|
||||
public NvApp(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public NvApp(String appName, int appId, boolean hdrSupported) {
|
||||
this.appName = appName;
|
||||
this.appId = appId;
|
||||
this.hdrSupported = hdrSupported;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
try {
|
||||
this.appId = Integer.parseInt(appId);
|
||||
this.initialized = true;
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed app ID: "+appId);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAppId(int appId) {
|
||||
this.appId = appId;
|
||||
this.initialized = true;
|
||||
}
|
||||
private String appName = "";
|
||||
private int appId;
|
||||
private boolean initialized;
|
||||
private boolean hdrSupported;
|
||||
|
||||
public NvApp() {}
|
||||
|
||||
public NvApp(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public NvApp(String appName, int appId, boolean hdrSupported) {
|
||||
this.appName = appName;
|
||||
this.appId = appId;
|
||||
this.hdrSupported = hdrSupported;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
try {
|
||||
this.appId = Integer.parseInt(appId);
|
||||
this.initialized = true;
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed app ID: "+appId);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAppId(int appId) {
|
||||
this.appId = appId;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setHdrSupported(boolean hdrSupported) {
|
||||
this.hdrSupported = hdrSupported;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return this.appName;
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
public void setHdrSupported(boolean hdrSupported) {
|
||||
this.hdrSupported = hdrSupported;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return this.appName;
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
|
||||
public boolean isHdrSupported() {
|
||||
return this.hdrSupported;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
public boolean isHdrSupported() {
|
||||
return this.hdrSupported;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,323 +18,326 @@ import java.util.Random;
|
||||
|
||||
public class PairingManager {
|
||||
|
||||
private NvHTTP http;
|
||||
|
||||
private PrivateKey pk;
|
||||
private X509Certificate cert;
|
||||
private SecretKey aesKey;
|
||||
private byte[] pemCertBytes;
|
||||
private NvHTTP http;
|
||||
|
||||
private PrivateKey pk;
|
||||
private X509Certificate cert;
|
||||
private SecretKey aesKey;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
private X509Certificate serverCert;
|
||||
|
||||
public enum PairState {
|
||||
NOT_PAIRED,
|
||||
PAIRED,
|
||||
PIN_WRONG,
|
||||
FAILED,
|
||||
ALREADY_IN_PROGRESS
|
||||
}
|
||||
|
||||
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
||||
this.http = http;
|
||||
this.cert = cryptoProvider.getClientCertificate();
|
||||
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
||||
this.pk = cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
|
||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
private static byte[] hexToBytes(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
||||
{
|
||||
String certText = NvHTTP.getXmlString(text, "plaincert");
|
||||
if (certText != null) {
|
||||
byte[] certBytes = hexToBytes(certText);
|
||||
private X509Certificate serverCert;
|
||||
|
||||
public enum PairState {
|
||||
NOT_PAIRED,
|
||||
PAIRED,
|
||||
PIN_WRONG,
|
||||
FAILED,
|
||||
ALREADY_IN_PROGRESS
|
||||
}
|
||||
|
||||
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
||||
this.http = http;
|
||||
this.cert = cryptoProvider.getClientCertificate();
|
||||
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
||||
this.pk = cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
|
||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
private static byte[] hexToBytes(String s) {
|
||||
int len = s.length();
|
||||
if (len % 2 != 0) {
|
||||
throw new IllegalArgumentException("Illegal string length: "+len);
|
||||
}
|
||||
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateRandomBytes(int length)
|
||||
{
|
||||
byte[] rand = new byte[length];
|
||||
new SecureRandom().nextBytes(rand);
|
||||
return rand;
|
||||
}
|
||||
|
||||
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
||||
byte[] saltedPin = new byte[salt.length + pin.length()];
|
||||
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
||||
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
||||
return saltedPin;
|
||||
}
|
||||
|
||||
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initVerify(cert.getPublicKey());
|
||||
sig.update(data);
|
||||
return sig.verify(signature);
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] signData(byte[] data, PrivateKey key) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initSign(key);
|
||||
sig.update(data);
|
||||
byte[] signature = new byte[256];
|
||||
sig.sign(signature, 0, signature.length);
|
||||
return signature;
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
||||
byte[] fullDecrypted = new byte[blockRoundedSize];
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
||||
{
|
||||
String certText = NvHTTP.getXmlString(text, "plaincert");
|
||||
if (certText != null) {
|
||||
byte[] certBytes = hexToBytes(certText);
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
cipher.doFinal(blockRoundedEncrypted, 0,
|
||||
blockRoundedSize, fullDecrypted);
|
||||
return fullDecrypted;
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateRandomBytes(int length)
|
||||
{
|
||||
byte[] rand = new byte[length];
|
||||
new SecureRandom().nextBytes(rand);
|
||||
return rand;
|
||||
}
|
||||
|
||||
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
||||
byte[] saltedPin = new byte[salt.length + pin.length()];
|
||||
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
||||
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
||||
return saltedPin;
|
||||
}
|
||||
|
||||
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initVerify(cert.getPublicKey());
|
||||
sig.update(data);
|
||||
return sig.verify(signature);
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] signData(byte[] data, PrivateKey key) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initSign(key);
|
||||
sig.update(data);
|
||||
byte[] signature = new byte[256];
|
||||
sig.sign(signature, 0, signature.length);
|
||||
return signature;
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
||||
byte[] fullDecrypted = new byte[blockRoundedSize];
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
return cipher.doFinal(blockRoundedData);
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||
return new SecretKeySpec(aesTruncated, "AES");
|
||||
}
|
||||
|
||||
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||
byte[] c = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, c, 0, a.length);
|
||||
System.arraycopy(b, 0, c, a.length, b.length);
|
||||
return c;
|
||||
}
|
||||
|
||||
public static String generatePinString() {
|
||||
Random r = new Random();
|
||||
return String.format((Locale)null, "%d%d%d%d",
|
||||
r.nextInt(10), r.nextInt(10),
|
||||
r.nextInt(10), r.nextInt(10));
|
||||
}
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
cipher.doFinal(blockRoundedEncrypted, 0,
|
||||
blockRoundedSize, fullDecrypted);
|
||||
return fullDecrypted;
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
||||
|
||||
public X509Certificate getPairedCert() {
|
||||
return serverCert;
|
||||
}
|
||||
|
||||
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
||||
PairingHashAlgorithm hashAlgo;
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
return cipher.doFinal(blockRoundedData);
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||
return new SecretKeySpec(aesTruncated, "AES");
|
||||
}
|
||||
|
||||
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||
byte[] c = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, c, 0, a.length);
|
||||
System.arraycopy(b, 0, c, a.length, b.length);
|
||||
return c;
|
||||
}
|
||||
|
||||
public static String generatePinString() {
|
||||
Random r = new Random();
|
||||
return String.format((Locale)null, "%d%d%d%d",
|
||||
r.nextInt(10), r.nextInt(10),
|
||||
r.nextInt(10), r.nextInt(10));
|
||||
}
|
||||
|
||||
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||
if (serverMajorVersion >= 7) {
|
||||
// Gen 7+ uses SHA-256 hashing
|
||||
hashAlgo = new Sha256PairingHash();
|
||||
}
|
||||
else {
|
||||
// Prior to Gen 7, SHA-1 is used
|
||||
hashAlgo = new Sha1PairingHash();
|
||||
}
|
||||
|
||||
// Generate a salt for hashing the PIN
|
||||
byte[] salt = generateRandomBytes(16);
|
||||
public X509Certificate getPairedCert() {
|
||||
return serverCert;
|
||||
}
|
||||
|
||||
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
||||
PairingHashAlgorithm hashAlgo;
|
||||
|
||||
// Combine the salt and pin, then create an AES key from them
|
||||
byte[] saltAndPin = saltPin(salt, pin);
|
||||
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||
|
||||
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||
// because the user must enter the PIN before the server responds
|
||||
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
||||
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||
false);
|
||||
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
||||
return PairState.FAILED;
|
||||
}
|
||||
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||
if (serverMajorVersion >= 7) {
|
||||
// Gen 7+ uses SHA-256 hashing
|
||||
hashAlgo = new Sha256PairingHash();
|
||||
}
|
||||
else {
|
||||
// Prior to Gen 7, SHA-1 is used
|
||||
hashAlgo = new Sha1PairingHash();
|
||||
}
|
||||
|
||||
// Generate a salt for hashing the PIN
|
||||
byte[] salt = generateRandomBytes(16);
|
||||
|
||||
// Save this cert for retrieval later
|
||||
serverCert = extractPlainCert(getCert);
|
||||
if (serverCert == null) {
|
||||
// Attempting to pair while another device is pairing will cause GFE
|
||||
// to give an empty cert in the response.
|
||||
return PairState.ALREADY_IN_PROGRESS;
|
||||
}
|
||||
// Combine the salt and pin, then create an AES key from them
|
||||
byte[] saltAndPin = saltPin(salt, pin);
|
||||
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||
|
||||
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||
// because the user must enter the PIN before the server responds
|
||||
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
||||
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||
false);
|
||||
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Require this cert for TLS to this host
|
||||
http.setServerCert(serverCert);
|
||||
|
||||
// Generate a random challenge and encrypt it with our AES key
|
||||
byte[] randomChallenge = generateRandomBytes(16);
|
||||
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||
|
||||
// Send the encrypted challenge to the server
|
||||
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Decode the server's response and subsequent challenge
|
||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||
|
||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||
|
||||
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||
byte[] clientSecret = generateRandomBytes(16);
|
||||
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Get the server's signed secret
|
||||
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
||||
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||
// Save this cert for retrieval later
|
||||
serverCert = extractPlainCert(getCert);
|
||||
if (serverCert == null) {
|
||||
// Attempting to pair while another device is pairing will cause GFE
|
||||
// to give an empty cert in the response.
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
// Ensure the authenticity of the data
|
||||
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Looks like a MITM
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Probably got the wrong PIN
|
||||
return PairState.PIN_WRONG;
|
||||
}
|
||||
|
||||
// Send the server our signed secret
|
||||
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Do the initial challenge (seems neccessary for us to show as paired)
|
||||
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
||||
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
// Require this cert for TLS to this host
|
||||
http.setServerCert(serverCert);
|
||||
|
||||
// Generate a random challenge and encrypt it with our AES key
|
||||
byte[] randomChallenge = generateRandomBytes(16);
|
||||
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||
|
||||
// Send the encrypted challenge to the server
|
||||
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Decode the server's response and subsequent challenge
|
||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||
|
||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||
|
||||
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||
byte[] clientSecret = generateRandomBytes(16);
|
||||
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Get the server's signed secret
|
||||
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
||||
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||
|
||||
return PairState.PAIRED;
|
||||
}
|
||||
|
||||
private interface PairingHashAlgorithm {
|
||||
int getHashLength();
|
||||
byte[] hashData(byte[] data);
|
||||
}
|
||||
|
||||
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure the authenticity of the data
|
||||
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Looks like a MITM
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Probably got the wrong PIN
|
||||
return PairState.PIN_WRONG;
|
||||
}
|
||||
|
||||
// Send the server our signed secret
|
||||
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Do the initial challenge (seems neccessary for us to show as paired)
|
||||
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
||||
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
return PairState.PAIRED;
|
||||
}
|
||||
|
||||
private interface PairingHashAlgorithm {
|
||||
int getHashLength();
|
||||
byte[] hashData(byte[] data);
|
||||
}
|
||||
|
||||
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class ControllerPacket {
|
||||
public static final short A_FLAG = 0x1000;
|
||||
public static final short B_FLAG = 0x2000;
|
||||
public static final short X_FLAG = 0x4000;
|
||||
public static final short Y_FLAG = (short)0x8000;
|
||||
public static final short UP_FLAG = 0x0001;
|
||||
public static final short DOWN_FLAG = 0x0002;
|
||||
public static final short LEFT_FLAG = 0x0004;
|
||||
public static final short RIGHT_FLAG = 0x0008;
|
||||
public static final short LB_FLAG = 0x0100;
|
||||
public static final short RB_FLAG = 0x0200;
|
||||
public static final short PLAY_FLAG = 0x0010;
|
||||
public static final short BACK_FLAG = 0x0020;
|
||||
public static final short LS_CLK_FLAG = 0x0040;
|
||||
public static final short RS_CLK_FLAG = 0x0080;
|
||||
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
||||
public static final short A_FLAG = 0x1000;
|
||||
public static final short B_FLAG = 0x2000;
|
||||
public static final short X_FLAG = 0x4000;
|
||||
public static final short Y_FLAG = (short)0x8000;
|
||||
public static final short UP_FLAG = 0x0001;
|
||||
public static final short DOWN_FLAG = 0x0002;
|
||||
public static final short LEFT_FLAG = 0x0004;
|
||||
public static final short RIGHT_FLAG = 0x0008;
|
||||
public static final short LB_FLAG = 0x0100;
|
||||
public static final short RB_FLAG = 0x0200;
|
||||
public static final short PLAY_FLAG = 0x0010;
|
||||
public static final short BACK_FLAG = 0x0020;
|
||||
public static final short LS_CLK_FLAG = 0x0040;
|
||||
public static final short RS_CLK_FLAG = 0x0080;
|
||||
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class KeyboardPacket {
|
||||
public static final byte KEY_DOWN = 0x03;
|
||||
public static final byte KEY_UP = 0x04;
|
||||
public static final byte KEY_DOWN = 0x03;
|
||||
public static final byte KEY_UP = 0x04;
|
||||
|
||||
public static final byte MODIFIER_SHIFT = 0x01;
|
||||
public static final byte MODIFIER_CTRL = 0x02;
|
||||
public static final byte MODIFIER_ALT = 0x04;
|
||||
public static final byte MODIFIER_SHIFT = 0x01;
|
||||
public static final byte MODIFIER_CTRL = 0x02;
|
||||
public static final byte MODIFIER_ALT = 0x04;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class MouseButtonPacket {
|
||||
public static final byte PRESS_EVENT = 0x07;
|
||||
public static final byte RELEASE_EVENT = 0x08;
|
||||
|
||||
public static final byte BUTTON_LEFT = 0x01;
|
||||
public static final byte BUTTON_MIDDLE = 0x02;
|
||||
public static final byte BUTTON_RIGHT = 0x03;
|
||||
public static final byte BUTTON_X1 = 0x04;
|
||||
public static final byte BUTTON_X2 = 0x05;
|
||||
public static final byte PRESS_EVENT = 0x07;
|
||||
public static final byte RELEASE_EVENT = 0x08;
|
||||
|
||||
public static final byte BUTTON_LEFT = 0x01;
|
||||
public static final byte BUTTON_MIDDLE = 0x02;
|
||||
public static final byte BUTTON_RIGHT = 0x03;
|
||||
public static final byte BUTTON_X1 = 0x04;
|
||||
public static final byte BUTTON_X2 = 0x05;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -84,9 +139,9 @@ public class MoonBridge {
|
||||
}
|
||||
}
|
||||
|
||||
public static int bridgeArInit(int audioConfiguration) {
|
||||
public static int bridgeArInit(int audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||
if (audioRenderer != null) {
|
||||
return audioRenderer.setup(audioConfiguration);
|
||||
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,11 +261,13 @@ 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);
|
||||
|
||||
public static native int getPendingAudioFrames();
|
||||
public static native int getPendingAudioDuration();
|
||||
|
||||
public static native int getPendingVideoFrames();
|
||||
|
||||
|
||||
@@ -4,62 +4,62 @@ import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class MdnsComputer {
|
||||
private InetAddress localAddr;
|
||||
private Inet6Address v6Addr;
|
||||
private String name;
|
||||
private InetAddress localAddr;
|
||||
private Inet6Address v6Addr;
|
||||
private String name;
|
||||
|
||||
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
|
||||
this.name = name;
|
||||
this.localAddr = localAddress;
|
||||
this.v6Addr = v6Addr;
|
||||
}
|
||||
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
|
||||
this.name = name;
|
||||
this.localAddr = localAddress;
|
||||
this.v6Addr = v6Addr;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public InetAddress getLocalAddress() {
|
||||
return localAddr;
|
||||
}
|
||||
public InetAddress getLocalAddress() {
|
||||
return localAddr;
|
||||
}
|
||||
|
||||
public Inet6Address getIpv6Address() {
|
||||
return v6Addr;
|
||||
}
|
||||
public Inet6Address getIpv6Address() {
|
||||
return v6Addr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof MdnsComputer) {
|
||||
MdnsComputer other = (MdnsComputer)o;
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof MdnsComputer) {
|
||||
MdnsComputer other = (MdnsComputer)o;
|
||||
|
||||
if (!other.name.equals(name)) {
|
||||
return false;
|
||||
}
|
||||
if (!other.name.equals(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((other.localAddr != null && localAddr == null) ||
|
||||
(other.localAddr == null && localAddr != null) ||
|
||||
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
|
||||
return false;
|
||||
}
|
||||
if ((other.localAddr != null && localAddr == null) ||
|
||||
(other.localAddr == null && localAddr != null) ||
|
||||
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((other.v6Addr != null && v6Addr == null) ||
|
||||
(other.v6Addr == null && v6Addr != null) ||
|
||||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
|
||||
return false;
|
||||
}
|
||||
if ((other.v6Addr != null && v6Addr == null) ||
|
||||
(other.v6Addr == null && v6Addr != null) ||
|
||||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,388 +21,388 @@ import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class MdnsDiscoveryAgent implements ServiceListener {
|
||||
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
|
||||
|
||||
private MdnsDiscoveryListener listener;
|
||||
private Thread discoveryThread;
|
||||
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
|
||||
private HashSet<String> pendingResolution = new HashSet<String>();
|
||||
|
||||
// The resolver factory's instance member has a static lifetime which
|
||||
// means our ref count and listener must be static also.
|
||||
private static int resolverRefCount = 0;
|
||||
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
|
||||
private static ServiceListener nvstreamListener = new ServiceListener() {
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceAdded(event);
|
||||
}
|
||||
}
|
||||
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
|
||||
|
||||
private MdnsDiscoveryListener listener;
|
||||
private Thread discoveryThread;
|
||||
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
|
||||
private HashSet<String> pendingResolution = new HashSet<String>();
|
||||
|
||||
// The resolver factory's instance member has a static lifetime which
|
||||
// means our ref count and listener must be static also.
|
||||
private static int resolverRefCount = 0;
|
||||
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
|
||||
private static ServiceListener nvstreamListener = new ServiceListener() {
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceAdded(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceRemoved(event);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceRemoved(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceResolved(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceResolved(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
|
||||
@Override
|
||||
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
|
||||
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
|
||||
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
|
||||
try {
|
||||
if (!networkInterface.isUp()) {
|
||||
return false;
|
||||
}
|
||||
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
|
||||
@Override
|
||||
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
|
||||
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
|
||||
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
|
||||
try {
|
||||
if (!networkInterface.isUp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
if (!networkInterface.supportsMulticast()) {
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
/*
|
||||
if (!networkInterface.supportsMulticast()) {
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
|
||||
if (networkInterface.isLoopback()) {
|
||||
return false;
|
||||
}
|
||||
if (networkInterface.isLoopback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static {
|
||||
// Override jmDNS's default topology discovery class with ours
|
||||
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
|
||||
@Override
|
||||
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
|
||||
return new MyNetworkTopologyDiscovery();
|
||||
}
|
||||
});
|
||||
}
|
||||
static {
|
||||
// Override jmDNS's default topology discovery class with ours
|
||||
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
|
||||
@Override
|
||||
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
|
||||
return new MyNetworkTopologyDiscovery();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static JmmDNS referenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
JmmDNS instance = JmmDNS.Factory.getInstance();
|
||||
if (++resolverRefCount == 1) {
|
||||
// This will cause the listener to be invoked for known hosts immediately.
|
||||
// JmDNS only supports one listener per service, so we have to do this here
|
||||
// with a static listener.
|
||||
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
private static JmmDNS referenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
JmmDNS instance = JmmDNS.Factory.getInstance();
|
||||
if (++resolverRefCount == 1) {
|
||||
// This will cause the listener to be invoked for known hosts immediately.
|
||||
// JmDNS only supports one listener per service, so we have to do this here
|
||||
// with a static listener.
|
||||
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
private static void dereferenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
if (--resolverRefCount == 0) {
|
||||
try {
|
||||
JmmDNS.Factory.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void dereferenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
if (--resolverRefCount == 0) {
|
||||
try {
|
||||
JmmDNS.Factory.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private void handleResolvedServiceInfo(ServiceInfo info) {
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.remove(info.getName());
|
||||
}
|
||||
private void handleResolvedServiceInfo(ServiceInfo info) {
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.remove(info.getName());
|
||||
}
|
||||
|
||||
try {
|
||||
handleServiceInfo(info);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Invalid DNS response
|
||||
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
handleServiceInfo(info);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Invalid DNS response
|
||||
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Inet6Address getLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
||||
return addr;
|
||||
}
|
||||
// fc00::/7 - ULAs
|
||||
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
private Inet6Address getLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
||||
return addr;
|
||||
}
|
||||
// fc00::/7 - ULAs
|
||||
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress()) {
|
||||
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress()) {
|
||||
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
|
||||
// First try to find a link local address, so we can match the interface identifier
|
||||
// with a global address (this will work for SLAAC but not DHCPv6).
|
||||
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
||||
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
|
||||
// First try to find a link local address, so we can match the interface identifier
|
||||
// with a global address (this will work for SLAAC but not DHCPv6).
|
||||
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
||||
|
||||
// We will try once to match a SLAAC interface suffix, then
|
||||
// pick the first matching address
|
||||
for (int tries = 0; tries < 2; tries++) {
|
||||
// We assume the addresses are already sorted in descending order
|
||||
// of preference from Bonjour.
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
|
||||
// Link-local, site-local, and loopback aren't global
|
||||
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// We will try once to match a SLAAC interface suffix, then
|
||||
// pick the first matching address
|
||||
for (int tries = 0; tries < 2; tries++) {
|
||||
// We assume the addresses are already sorted in descending order
|
||||
// of preference from Bonjour.
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
|
||||
// Link-local, site-local, and loopback aren't global
|
||||
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] addrBytes = addr.getAddress();
|
||||
byte[] addrBytes = addr.getAddress();
|
||||
|
||||
// 2002::/16
|
||||
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
|
||||
// 6to4 has horrible performance
|
||||
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// 2001::/32
|
||||
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
|
||||
// Teredo also has horrible performance
|
||||
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// fc00::/7
|
||||
else if ((addrBytes[0] & 0xfe) == 0xfc) {
|
||||
// ULAs aren't global
|
||||
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// 2002::/16
|
||||
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
|
||||
// 6to4 has horrible performance
|
||||
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// 2001::/32
|
||||
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
|
||||
// Teredo also has horrible performance
|
||||
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// fc00::/7
|
||||
else if ((addrBytes[0] & 0xfe) == 0xfc) {
|
||||
// ULAs aren't global
|
||||
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare the final 64-bit interface identifier and skip the address
|
||||
// if it doesn't match our link-local address.
|
||||
if (linkLocalAddr != null && tries == 0) {
|
||||
boolean matched = true;
|
||||
// Compare the final 64-bit interface identifier and skip the address
|
||||
// if it doesn't match our link-local address.
|
||||
if (linkLocalAddr != null && tries == 0) {
|
||||
boolean matched = true;
|
||||
|
||||
for (int i = 8; i < 16; i++) {
|
||||
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 8; i < 16; i++) {
|
||||
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
|
||||
Inet4Address v4Addrs[] = info.getInet4Addresses();
|
||||
Inet6Address v6Addrs[] = info.getInet6Addresses();
|
||||
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
|
||||
Inet4Address v4Addrs[] = info.getInet4Addresses();
|
||||
Inet6Address v6Addrs[] = info.getInet6Addresses();
|
||||
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
|
||||
|
||||
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
|
||||
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
|
||||
|
||||
// Add a computer object for each IPv4 address reported by the PC
|
||||
for (Inet4Address v4Addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
|
||||
if (computers.put(computer.getLocalAddress(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add a computer object for each IPv4 address reported by the PC
|
||||
for (Inet4Address v4Addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
|
||||
if (computers.put(computer.getLocalAddress(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there were no IPv4 addresses, use IPv6 for registration
|
||||
if (v4Addrs.length == 0) {
|
||||
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
||||
// If there were no IPv4 addresses, use IPv6 for registration
|
||||
if (v4Addrs.length == 0) {
|
||||
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
||||
|
||||
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
|
||||
if (computers.put(v6LocalAddr != null ?
|
||||
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startDiscovery(final int discoveryIntervalMs) {
|
||||
// Kill any existing discovery before starting a new one
|
||||
stopDiscovery();
|
||||
|
||||
// Add our listener to the set
|
||||
synchronized (listeners) {
|
||||
listeners.add(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
discoveryThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// This may result in listener callbacks so we must register
|
||||
// our listener first.
|
||||
JmmDNS resolver = referenceResolver();
|
||||
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
// Start an mDNS request
|
||||
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
|
||||
|
||||
// Run service resolution again for pending machines
|
||||
ArrayList<String> pendingNames;
|
||||
synchronized (pendingResolution) {
|
||||
pendingNames = new ArrayList<String>(pendingResolution);
|
||||
}
|
||||
for (String name : pendingNames) {
|
||||
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
|
||||
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
|
||||
if (infos != null && infos.length != 0) {
|
||||
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
|
||||
for (ServiceInfo svcinfo : infos) {
|
||||
handleResolvedServiceInfo(svcinfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the next polling interval
|
||||
try {
|
||||
Thread.sleep(discoveryIntervalMs);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Dereference the resolver
|
||||
dereferenceResolver();
|
||||
}
|
||||
}
|
||||
};
|
||||
discoveryThread.setName("mDNS Discovery Thread");
|
||||
discoveryThread.start();
|
||||
}
|
||||
|
||||
public void stopDiscovery() {
|
||||
// Remove our listener from the set
|
||||
synchronized (listeners) {
|
||||
listeners.remove(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
// If there's already a running thread, interrupt it
|
||||
if (discoveryThread != null) {
|
||||
discoveryThread.interrupt();
|
||||
discoveryThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<MdnsComputer> getComputerSet() {
|
||||
synchronized (computers) {
|
||||
return new ArrayList<MdnsComputer>(computers.values());
|
||||
}
|
||||
}
|
||||
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
|
||||
if (computers.put(v6LocalAddr != null ?
|
||||
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startDiscovery(final int discoveryIntervalMs) {
|
||||
// Kill any existing discovery before starting a new one
|
||||
stopDiscovery();
|
||||
|
||||
// Add our listener to the set
|
||||
synchronized (listeners) {
|
||||
listeners.add(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
discoveryThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// This may result in listener callbacks so we must register
|
||||
// our listener first.
|
||||
JmmDNS resolver = referenceResolver();
|
||||
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
// Start an mDNS request
|
||||
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
|
||||
|
||||
// Run service resolution again for pending machines
|
||||
ArrayList<String> pendingNames;
|
||||
synchronized (pendingResolution) {
|
||||
pendingNames = new ArrayList<String>(pendingResolution);
|
||||
}
|
||||
for (String name : pendingNames) {
|
||||
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
|
||||
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
|
||||
if (infos != null && infos.length != 0) {
|
||||
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
|
||||
for (ServiceInfo svcinfo : infos) {
|
||||
handleResolvedServiceInfo(svcinfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the next polling interval
|
||||
try {
|
||||
Thread.sleep(discoveryIntervalMs);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Dereference the resolver
|
||||
dereferenceResolver();
|
||||
}
|
||||
}
|
||||
};
|
||||
discoveryThread.setName("mDNS Discovery Thread");
|
||||
discoveryThread.start();
|
||||
}
|
||||
|
||||
public void stopDiscovery() {
|
||||
// Remove our listener from the set
|
||||
synchronized (listeners) {
|
||||
listeners.remove(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
// If there's already a running thread, interrupt it
|
||||
if (discoveryThread != null) {
|
||||
discoveryThread.interrupt();
|
||||
discoveryThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<MdnsComputer> getComputerSet() {
|
||||
synchronized (computers) {
|
||||
return new ArrayList<MdnsComputer>(computers.values());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
|
||||
|
||||
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
|
||||
if (info == null) {
|
||||
// This machine is pending resolution
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.add(event.getInfo().getName());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.info("mDNS: Resolved (blocking)");
|
||||
handleResolvedServiceInfo(info);
|
||||
}
|
||||
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
|
||||
if (info == null) {
|
||||
// This machine is pending resolution
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.add(event.getInfo().getName());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.info("mDNS: Resolved (blocking)");
|
||||
handleResolvedServiceInfo(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
|
||||
|
||||
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
|
||||
for (Inet4Address addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
|
||||
for (Inet4Address addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
|
||||
for (Inet6Address addr : v6Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
|
||||
for (Inet6Address addr : v6Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
// We handle this synchronously
|
||||
}
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
// We handle this synchronously
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.limelight.nvstream.mdns;
|
||||
|
||||
public interface MdnsDiscoveryListener {
|
||||
void notifyComputerAdded(MdnsComputer computer);
|
||||
void notifyComputerRemoved(MdnsComputer computer);
|
||||
void notifyDiscoveryFailure(Exception e);
|
||||
void notifyComputerAdded(MdnsComputer computer);
|
||||
void notifyComputerRemoved(MdnsComputer computer);
|
||||
void notifyDiscoveryFailure(Exception e);
|
||||
}
|
||||
|
||||
@@ -10,90 +10,90 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
public class WakeOnLanSender {
|
||||
private static final int[] PORTS_TO_TRY = new int[] {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
|
||||
};
|
||||
|
||||
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
||||
DatagramSocket sock = new DatagramSocket(0);
|
||||
byte[] payload = createWolPayload(computer);
|
||||
IOException lastException = null;
|
||||
boolean sentWolPacket = false;
|
||||
private static final int[] PORTS_TO_TRY = new int[] {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
|
||||
};
|
||||
|
||||
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
||||
DatagramSocket sock = new DatagramSocket(0);
|
||||
byte[] payload = createWolPayload(computer);
|
||||
IOException lastException = null;
|
||||
boolean sentWolPacket = false;
|
||||
|
||||
try {
|
||||
// Try all resolved remote and local addresses and IPv4 broadcast address.
|
||||
// The broadcast address is required to avoid stale ARP cache entries
|
||||
// making the sleeping machine unreachable.
|
||||
for (String unresolvedAddress : new String[] {
|
||||
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
|
||||
}) {
|
||||
try {
|
||||
// Try all resolved remote and local addresses and IPv4 broadcast address.
|
||||
// The broadcast address is required to avoid stale ARP cache entries
|
||||
// making the sleeping machine unreachable.
|
||||
for (String unresolvedAddress : new String[] {
|
||||
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
|
||||
}) {
|
||||
if (unresolvedAddress == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
|
||||
// Try all the ports for each resolved address
|
||||
for (int port : PORTS_TO_TRY) {
|
||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||
dp.setAddress(resolvedAddress);
|
||||
dp.setPort(port);
|
||||
sock.send(dp);
|
||||
sentWolPacket = true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// We may have addresses that don't resolve on this subnet,
|
||||
// but don't throw and exit the whole function if that happens.
|
||||
// We'll throw it at the end if we didn't send a single packet.
|
||||
e.printStackTrace();
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sock.close();
|
||||
}
|
||||
try {
|
||||
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
|
||||
// Try all the ports for each resolved address
|
||||
for (int port : PORTS_TO_TRY) {
|
||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||
dp.setAddress(resolvedAddress);
|
||||
dp.setPort(port);
|
||||
sock.send(dp);
|
||||
sentWolPacket = true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// We may have addresses that don't resolve on this subnet,
|
||||
// but don't throw and exit the whole function if that happens.
|
||||
// We'll throw it at the end if we didn't send a single packet.
|
||||
e.printStackTrace();
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sock.close();
|
||||
}
|
||||
|
||||
// Propagate the DNS resolution exception if we didn't
|
||||
// manage to get a single packet out to the host.
|
||||
if (!sentWolPacket && lastException != null) {
|
||||
throw lastException;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] macStringToBytes(String macAddress) {
|
||||
byte[] macBytes = new byte[6];
|
||||
@SuppressWarnings("resource")
|
||||
Scanner scan = new Scanner(macAddress).useDelimiter(":");
|
||||
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
||||
try {
|
||||
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
scan.close();
|
||||
return macBytes;
|
||||
}
|
||||
|
||||
private static byte[] createWolPayload(ComputerDetails computer) {
|
||||
byte[] payload = new byte[102];
|
||||
byte[] macAddress = macStringToBytes(computer.macAddress);
|
||||
int i;
|
||||
|
||||
// 6 bytes of FF
|
||||
for (i = 0; i < 6; i++) {
|
||||
payload[i] = (byte)0xFF;
|
||||
}
|
||||
|
||||
// 16 repetitions of the MAC address
|
||||
for (int j = 0; j < 16; j++) {
|
||||
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
|
||||
i += macAddress.length;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
// Propagate the DNS resolution exception if we didn't
|
||||
// manage to get a single packet out to the host.
|
||||
if (!sentWolPacket && lastException != null) {
|
||||
throw lastException;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] macStringToBytes(String macAddress) {
|
||||
byte[] macBytes = new byte[6];
|
||||
@SuppressWarnings("resource")
|
||||
Scanner scan = new Scanner(macAddress).useDelimiter(":");
|
||||
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
||||
try {
|
||||
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
scan.close();
|
||||
return macBytes;
|
||||
}
|
||||
|
||||
private static byte[] createWolPayload(ComputerDetails computer) {
|
||||
byte[] payload = new byte[102];
|
||||
byte[] macAddress = macStringToBytes(computer.macAddress);
|
||||
int i;
|
||||
|
||||
// 6 bytes of FF
|
||||
for (i = 0; i < 6; i++) {
|
||||
payload[i] = (byte)0xFF;
|
||||
}
|
||||
|
||||
// 16 repetitions of the MAC address
|
||||
for (int j = 0; j < 16; j++) {
|
||||
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
|
||||
i += macAddress.length;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,12 +104,6 @@ public class AddComputerManually extends Activity {
|
||||
try {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
details.manualAddress = host;
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(host, managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(this));
|
||||
details.serverCert = http.getCertificateIfTrusted();
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
success = managerBinder.addComputerBlocking(details);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||
|
||||
@@ -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,42 @@ 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, where the feature is not available (some low RAM devices),
|
||||
// and on Fire OS where it violates the Amazon App Store guidelines for some reason.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
|
||||
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture") ||
|
||||
getActivity().getPackageManager().hasSystemFeature("com.amazon.software.fireos")) {
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_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 +265,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 +326,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;
|
||||
}
|
||||
|
||||
32
app/src/main/java/com/limelight/utils/NetHelper.java
Normal file
32
app/src/main/java/com/limelight/utils/NetHelper.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
|
||||
public class NetHelper {
|
||||
public static boolean isActiveNetworkVpn(Context context) {
|
||||
ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Network activeNetwork = connMgr.getActiveNetwork();
|
||||
if (activeNetwork != null) {
|
||||
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
|
||||
if (netCaps != null) {
|
||||
return netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||
if (activeNetworkInfo != null) {
|
||||
return activeNetworkInfo.getType() == ConnectivityManager.TYPE_VPN;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
public class Vector2d {
|
||||
private float x;
|
||||
private float y;
|
||||
private double magnitude;
|
||||
|
||||
public static final Vector2d ZERO = new Vector2d();
|
||||
|
||||
public Vector2d() {
|
||||
initialize(0, 0);
|
||||
}
|
||||
|
||||
public void initialize(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
}
|
||||
|
||||
public double getMagnitude() {
|
||||
return magnitude;
|
||||
}
|
||||
|
||||
public void getNormalized(Vector2d vector) {
|
||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||
}
|
||||
|
||||
public void scalarMultiply(double factor) {
|
||||
initialize((float)(x * factor), (float)(y * factor));
|
||||
}
|
||||
|
||||
public void setX(float x) {
|
||||
initialize(x, this.y);
|
||||
}
|
||||
|
||||
public void setY(float y) {
|
||||
initialize(this.x, y);
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
private float x;
|
||||
private float y;
|
||||
private double magnitude;
|
||||
|
||||
public static final Vector2d ZERO = new Vector2d();
|
||||
|
||||
public Vector2d() {
|
||||
initialize(0, 0);
|
||||
}
|
||||
|
||||
public void initialize(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
}
|
||||
|
||||
public double getMagnitude() {
|
||||
return magnitude;
|
||||
}
|
||||
|
||||
public void getNormalized(Vector2d vector) {
|
||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||
}
|
||||
|
||||
public void scalarMultiply(double factor) {
|
||||
initialize((float)(x * factor), (float)(y * factor));
|
||||
}
|
||||
|
||||
public void setX(float x) {
|
||||
initialize(x, this.y);
|
||||
}
|
||||
|
||||
public void setY(float y) {
|
||||
initialize(this.x, y);
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,16 +80,16 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
|
||||
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
|
||||
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
|
||||
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I");
|
||||
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(I)I");
|
||||
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(III)I");
|
||||
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
|
||||
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
|
||||
BridgeArCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArCleanup", "()V");
|
||||
BridgeArPlaySampleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArPlaySample", "([S)V");
|
||||
BridgeClStageStartingMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageStarting", "(I)V");
|
||||
BridgeClStageCompleteMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageComplete", "(I)V");
|
||||
BridgeClStageFailedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageFailed", "(IJ)V");
|
||||
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);
|
||||
@@ -176,8 +157,10 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
|
||||
|
||||
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
||||
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
|
||||
decodeUnit->frameNumber, decodeUnit->receiveTimeMs);
|
||||
decodeUnit->frameNumber, (jlong)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);
|
||||
(jlong)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;
|
||||
|
||||
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration, opusConfig->sampleRate, opusConfig->samplesPerFrame);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration);
|
||||
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, ...) {
|
||||
@@ -382,6 +346,7 @@ static AUDIO_RENDERER_CALLBACKS BridgeAudioRendererCallbacks = {
|
||||
.stop = BridgeArStop,
|
||||
.cleanup = BridgeArCleanup,
|
||||
.decodeAndPlaySample = BridgeArDecodeAndPlaySample,
|
||||
.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION
|
||||
};
|
||||
|
||||
static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
|
||||
|
||||
Submodule app/src/main/jni/moonlight-core/moonlight-common-c updated: 7de68207f0...5c9935e11b
@@ -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();
|
||||
@@ -83,8 +94,8 @@ Java_com_limelight_nvstream_jni_MoonBridge_findExternalAddressIP4(JNIEnv *env, j
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_nvstream_jni_MoonBridge_getPendingAudioFrames(JNIEnv *env, jclass clazz) {
|
||||
return LiGetPendingAudioFrames();
|
||||
Java_com_limelight_nvstream_jni_MoonBridge_getPendingAudioDuration(JNIEnv *env, jclass clazz) {
|
||||
return LiGetPendingAudioDuration();
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
|
||||
8
app/src/main/res/anim/boxart_fadein.xml
Normal file
8
app/src/main/res/anim/boxart_fadein.xml
Normal 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
app/src/main/res/anim/boxart_fadeout.xml
Normal file
8
app/src/main/res/anim/boxart_fadeout.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable-xhdpi/no_app_image.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/no_app_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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">
|
||||
<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_image"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="175dp">
|
||||
</ImageView>
|
||||
<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>
|
||||
@@ -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">
|
||||
<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_image"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="117dp">
|
||||
</ImageView>
|
||||
<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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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"/>
|
||||
@@ -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>
|
||||
14
app/src/main/res/values-de/arrays.xml
Normal file
14
app/src/main/res/values-de/arrays.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="decoder_names">
|
||||
<item>Decoder automatisch auswählen</item>
|
||||
<item>Software Decodierung erzwingen</item>
|
||||
<item>Hardware Decodierung erzwingen</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="video_format_names">
|
||||
<item>Verwende H.265 so fern stabile Unterstützung vorhanden ist</item>
|
||||
<item>Immer H.265 verwenden (könnte Crashes verursachen)</item>
|
||||
<item>Nie H.265 verwenden</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
188
app/src/main/res/values-de/strings.xml
Normal file
188
app/src/main/res/values-de/strings.xml
Normal file
@@ -0,0 +1,188 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Shortcut strings -->
|
||||
<string name="scut_deleted_pc">PC gelöscht</string>
|
||||
<string name="scut_not_paired">PC nicht verbunden</string>
|
||||
<string name="scut_pc_not_found">PC nicht gefunden</string>
|
||||
<string name="scut_invalid_uuid">Angegebener PC ist unzulässig</string>
|
||||
<string name="scut_invalid_app_id">Angegebene App ist unzulässig</string>
|
||||
|
||||
<!-- Help strings -->
|
||||
<string name="help_loading_title">Hilfsanzeige</string>
|
||||
<string name="help_loading_msg">Lade Hilfesanzeige…</string>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list">Spiele List anzeigen</string>
|
||||
<string name="pcview_menu_pair_pc">Mit PC verbinden</string>
|
||||
<string name="pcview_menu_unpair_pc">Verbindung beenden</string>
|
||||
<string name="pcview_menu_send_wol">Wake-On-LAN Anfrage senden</string>
|
||||
<string name="pcview_menu_delete_pc">PC löschen</string>
|
||||
<string name="pcview_menu_details">Details anzeigen</string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing">Verbinden…</string>
|
||||
<string name="pair_pc_offline">Computer ist offline</string>
|
||||
<string name="pair_pc_ingame">Am Computer ist bereits ein Spiel aktiv. Sie müssen das Spiel beenden bevor sie sich koppeln können.</string>
|
||||
<string name="pair_pairing_title">Verbinden</string>
|
||||
<string name="pair_pairing_msg">Bitte geben sie folgenden PIN auf ihrem PC ein:</string>
|
||||
<string name="pair_incorrect_pin">PIN inkorrekt</string>
|
||||
<string name="pair_fail">Verbindung fehlgeschlagen</string>
|
||||
<string name="pair_already_in_progress">Verbindungsaufbau rebreits im gange</string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online">Computer ist online</string>
|
||||
<string name="wol_no_mac">PC konnte nicht geweckt werden, da GFE keine MAC-Adresse gesendet hat.</string>
|
||||
<string name="wol_waking_pc">PC wird geweckt…</string>
|
||||
<string name="wol_waking_msg">Es kann einige Momente dauert ihren PC zu wecken.
|
||||
Sollte dies fehlschlagen stellen sie bitte sicher, dass Wake-On-LAN korrekt konfiguriert ist.
|
||||
</string>
|
||||
<string name="wol_fail">Sender der Wake-On-LAN Pakete ist fehlgeschlagen</string>
|
||||
|
||||
<!-- Unpair messages -->
|
||||
<string name="unpairing">Trenne Verbindung…</string>
|
||||
<string name="unpair_success">Verbindung erfolgreich getrennt</string>
|
||||
<string name="unpair_fail">Trennung der Verbindung fehlgeschlagen</string>
|
||||
<string name="unpair_error">Gerät wurde nicht verbunden</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline">Computer ist offline</string>
|
||||
<string name="error_manager_not_running">Der ComputerManager Service läuft nicht. Bitte warten sie ein paar Sekunden oder starten sie Die App neu.</string>
|
||||
<string name="error_unknown_host">Host konnte nicht aufgelößt werden.</string>
|
||||
<string name="error_404">GFE ist auf einen HTTP 404 Fehler gestoßen. Stellen sie sicher, dass die GPU ihres PCs unterstützt wird.
|
||||
Die verwendung von Remote-Desktop Software kann ebenso diesen Fehler verursachen. Starten sie ihren Computer neu oder reinstallieren sie GFE.
|
||||
</string>
|
||||
<string name="title_decoding_error">Video Decoder ist gecrashed</string>
|
||||
<string name="message_decoding_error">Moonlight ist wegen einer Inkompatibilität zu dem Video-Decoder ihres Gerätes gecrasht. Stellen sie sicher, dass GeForce Experience Version auf ihrem PC auf dem neuesten Stand ist. Sollten weiterin Crashes auftreten, versuchen sie ihre Stream Einstellungen zu justieren.</string>
|
||||
<string name="title_decoding_reset">Video Einstellungen zurücksetzen</string>
|
||||
<string name="message_decoding_reset">Der Video-Decoder ihres Geärts ist wiederholt mit den ausgewählten Einstellungen gecrasht. Ihre streaming einstellungen wurden zurückgesetzt.</string>
|
||||
<string name="error_usb_prohibited">USB Zugriff ist administrativ unterbunden. Bitte überprüfen sie ihre Knox oder MDM Einstellungen.</string>
|
||||
<string name="unable_to_pin_shortcut">Die zur Zeit aktive Launcher App ünterstützt das erstellen angehefteter Shortcuts nicht.</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">Verbindung herstellen</string>
|
||||
<string name="conn_establishing_msg">Verbindung starten</string>
|
||||
<string name="conn_metered">Warnung: Das Datentransfairvolument ihrer Netzwerkverbindung ist limitiert!</string>
|
||||
<string name="conn_client_latency">Durchschnittliche Frame-Dekodierungslatenz:</string>
|
||||
<string name="conn_client_latency_hw">Hardware-Dekodierungslatenz:</string>
|
||||
<string name="conn_hardware_latency">Durchschnittliche Hardware-Dekodierungslatenz:</string>
|
||||
<string name="conn_starting">Startet</string>
|
||||
<string name="conn_error_title">Verbindingsfehler</string>
|
||||
<string name="conn_error_msg">Start fehlgeschlagen</string>
|
||||
<string name="conn_terminated_title">Verbindung beendet</string>
|
||||
<string name="conn_terminated_msg">Die verbinding wurde beendet</string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">IP-Adresse des GeForce PCs</string>
|
||||
<string name="searching_pc">Suche nach PCs wo GeForce Experience aktiv ist…</string>
|
||||
<string name="yes">Ja</string>
|
||||
<string name="no">Nein</string>
|
||||
<string name="lost_connection">Verbindung zum PC verloren</string>
|
||||
<string name="title_details">Details</string>
|
||||
<string name="help">Hilfe</string>
|
||||
<string name="delete_pc_msg">Sind sie sicher, dass sie diesen PC löschen möchten?</string>
|
||||
<string name="slow_connection_msg">Langsame Verbindung zum PC\nReduzieren sie die Bitrate</string>
|
||||
<string name="poor_connection_msg">Sehr langsame Verbindung zum PC</string>
|
||||
<string name="perf_overlay_text">Videodimensionen: %1$s\nDecoder: %2$s\nGeschätzte PC Bildwiederholrate: %3$.2f FPS\nBildwiederholrate der Netzwerkübertragung: %4$.2f FPS\nWiedergabe-Bildwiederholungsrate: %5$.2f FPS\nWegen Netzwerkübertraung ausgelassene Frames: %6$.2f%%\nDurchschnittliche Übertragunsdauer: %7$.2f ms\nDurchschnittliche decoding dauer: %8$.2f ms</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_connect_msg">Verbinde mit PC…</string>
|
||||
<string name="applist_menu_resume">Sitzung Wiederherstellen</string>
|
||||
<string name="applist_menu_quit">Sitzung Beenden</string>
|
||||
<string name="applist_menu_quit_and_start">Aktuelles Spiel beenden und Starte</string>
|
||||
<string name="applist_menu_cancel">Abbrechen</string>
|
||||
<string name="applist_menu_details">Details anzeigen</string>
|
||||
<string name="applist_menu_scut">Shortcut erstellen</string>
|
||||
<string name="applist_menu_tv_channel">Zu Kanal hinzufügen</string>
|
||||
<string name="applist_refresh_title">App Liste</string>
|
||||
<string name="applist_refresh_msg">Aktualisiere Apps…</string>
|
||||
<string name="applist_refresh_error_title">Fehler</string>
|
||||
<string name="applist_refresh_error_msg">Abfrage der App-Liste fehlgeschlagen</string>
|
||||
<string name="applist_quit_app">Beenden</string>
|
||||
<string name="applist_quit_success">Erfolgreich Beendet</string>
|
||||
<string name="applist_quit_fail">Beenden fehlgeschlagen</string>
|
||||
<string name="applist_quit_confirmation">Sind sie sicher, dass die die laufende App schließen möchten? Ungespeicherte Daten gehen verloren.</string>
|
||||
<string name="applist_details_id">App ID:</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc">PC manuell hinzufügen</string>
|
||||
<string name="msg_add_pc">Verbinde zum PC…</string>
|
||||
<string name="addpc_fail">Verbindung fehlgeschlagen. Stellen sie sicher, dass die benötigten ports von einer Firewall gefiltert werden.</string>
|
||||
<string name="addpc_success">Computer erfolgreich hinzugefügt</string>
|
||||
<string name="addpc_unknown_host">Auflößen der PC-Adresse fehlgeschlagen. Stellen sie sicher, dass die Adresse keine Tippfehler beinhaltet.</string>
|
||||
<string name="addpc_enter_ip">Sie müssen eine IP-Addresse eingeben.</string>
|
||||
<string name="addpc_wrong_sitelocal">Diese Adresse sieht falsch aus. Sie müssen die öffentliche IP-Adresse ihreres Routers benutzen um über das Internet zu streamen.</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Allgemeine Einstellungen</string>
|
||||
<string name="title_resolution_list">Videoauflösung</string>
|
||||
<string name="summary_resolution_list">Ehöhen für klarere Bilder. Reduzieren für flüssigere Darstellung auf langsameren Geräten und Netzwerken.</string>
|
||||
<string name="title_fps_list">Bildwiederholungsrate</string>
|
||||
<string name="summary_fps_list">Erhöhen für einen gleichmäßigeren Video-Stream. Verringern um auf langsameren Geräten eine bessere Performance zu erzielen.</string>
|
||||
<string name="title_seekbar_bitrate">Video bitrate</string>
|
||||
<string name="summary_seekbar_bitrate">Erhöhen für einen schärferen Video-Stream. Verringern um auf langsameren Geräten eine bessere Performance zu erzielen.</string>
|
||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
||||
<string name="title_unlock_fps">Alle Bildwiederholungsraten freigeben</string>
|
||||
<string name="summary_unlock_fps">Streaming mit 90 oder 120 FPS reduziert gegebenfalls die Latenz auf High-End Geräten, für jedoch zu Crashes oder Lag auf Geräten die dies nicht untersützen können.</string>
|
||||
<string name="title_checkbox_stretch_video">Video auf den ganzen Bildschirm ausdehnen</string>
|
||||
<string name="title_checkbox_disable_warnings">Warnhinweise deaktivieren</string>
|
||||
<string name="summary_checkbox_disable_warnings">Verbindungswarnungen nicht als Overlay wärend des Streamens anzeigen</string>
|
||||
<string name="title_checkbox_enable_pip">Bild-in-Bild Überwachungsmodus aktivieren</string>
|
||||
<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="category_input_settings">Eingabe Einstellungen</string>
|
||||
<string name="title_checkbox_multi_controller">Automatische GamePad-Erkennung</string>
|
||||
<string name="summary_checkbox_multi_controller">Abwählen dieser Option erzwingt dass immer ein GamePad present ist</string>
|
||||
<string name="title_checkbox_vibrate_fallback">Vibrationsemulation aktivieren</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">Lässt das Gerät vibrieren falls das GamePad keine Vibration unterstütz</string>
|
||||
<string name="title_seekbar_deadzone">Tot Bereich des Analogsticks</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Xbox 360/One GamePad Treiber</string>
|
||||
<string name="summary_checkbox_xb1_driver">Aktiviert eingebauten USB Treiber für Geräte die keinen Xbox GamePad-Unterstützung haben</string>
|
||||
<string name="title_checkbox_usb_bind_all">Android GamePad Unterstütung überlagern</string>
|
||||
<string name="summary_checkbox_usb_bind_all">Erzwingt die Vernwedung von Moonlight\'s USB Treiber für alle Xbox kompatiblen GamePads</string>
|
||||
<string name="title_checkbox_mouse_emulation">Maus Emulation via GamePad</string>
|
||||
<string name="summary_checkbox_mouse_emulation">Langes gedrückt halten der Start-Taste wechselt in den Maus Modus</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Vor- und Zurück-Tasten aktivieren</string>
|
||||
<string name="summary_checkbox_mouse_nav_buttons">Aktivierung dieser Option kann auf fehleranfälligen Geräten Rechts-Clicks verunmöglichen</string>
|
||||
|
||||
<string name="category_on_screen_controls_settings">On-Screen Steuerungseinstellungen</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">Zeige On-Screen Steuerung</string>
|
||||
<string name="summary_checkbox_show_onscreen_controls">Zeige virtuelle Steuerelemente als Overlay auf dem Touchscreen</string>
|
||||
<string name="title_checkbox_vibrate_osc">Vibrationen aktivieren</string>
|
||||
<string name="summary_checkbox_vibrate_osc">Vibrieren als haptisches Feedback für die On-Screen Steuerelemente</string>
|
||||
<string name="title_only_l3r3">Nur L3 und R3 anzeigen</string>
|
||||
<string name="summary_only_l3r3">Alle virtuellen Steuerelemente außer L3 und R3 verbregen.</string>
|
||||
<string name="title_reset_osc">Gespeicherte On-Screen Steuerungelemente zurücksetzen</string>
|
||||
<string name="summary_reset_osc">Alle On-Screen Steuerelemente auf ihre Vorgabegröße und Position zurück setzen</string>
|
||||
<string name="dialog_title_reset_osc">Layout Zurücksetzen</string>
|
||||
<string name="dialog_text_reset_osc">Sind sie sicher, dass sie das gepseicherte on-screen Layout der Steuerelemte löschen wollen?</string>
|
||||
<string name="toast_reset_osc_success">On-Screen Steuerelemente wurden zurückgesetzt</string>
|
||||
|
||||
<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_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>
|
||||
|
||||
<string name="category_host_settings">Host Einstellungen</string>
|
||||
<string name="title_checkbox_enable_sops">Spieleinstellungen optimieren</string>
|
||||
<string name="summary_checkbox_enable_sops">GFE erlauben die die Spieleinstellungen zu optimieren.</string>
|
||||
<string name="title_checkbox_host_audio">Audio auf dem PC wiedergeben</string>
|
||||
<string name="summary_checkbox_host_audio">Audio auf dem PC und auf diesem Gerät wiedergeben</string>
|
||||
|
||||
<string name="category_advanced_settings">Erweiterte Einstellungen</string>
|
||||
<string name="title_disable_frame_drop">Nie Frames Überspringen</string>
|
||||
<string name="summary_disable_frame_drop">Kann potentiell das Mikro-Ruckeln auf einigen Geräten reduzieren, allerdings erhöt dies gleichzeitig die Latenz</string>
|
||||
<string name="title_video_format">Ändere H.265 Einstellungen</string>
|
||||
<string name="summary_video_format">H.265 verringerd die Video-Bandbreitenanforderung, funktioniert allerdings nur auf sehr neuen Geräten</string>
|
||||
<string name="title_enable_hdr">HDR aktivieren (experimentell)</string>
|
||||
<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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
24
app/src/main/res/values-fr/arrays.xml
Normal file
24
app/src/main/res/values-fr/arrays.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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>
|
||||
<item>N\'utilisez jamais H.265</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -53,7 +53,7 @@
|
||||
L\'utilisation d\'un logiciel de bureau à distance peut également provoquer cette erreur. Essayez de redémarrer votre machine ou de réinstaller GFE.
|
||||
</string>
|
||||
<string name="title_decoding_error">Le décodeur vidéo s\'est écrasé</string>
|
||||
<string name="message_decoding_error">Moonlight s\'est écrasé en raison d\'une incompatibilité avec le décodeur vidéo de cet appareil. Assurez-vous que GeForce Experience soit mis à jour vers la dernière version sur votre PC. Essayez de régler les paramètres de diffusion si les plantages continuent.</string>
|
||||
<string name="message_decoding_error">Moonlight s\'est arrêté en raison d\'une incompatibilité avec le décodeur vidéo de cet appareil. Assurez-vous que GeForce Experience soit mis à jour vers la dernière version sur votre PC. Essayez de régler les paramètres de diffusion si les plantages continuent.</string>
|
||||
<string name="title_decoding_reset">Paramètres vidéo réinitialiser</string>
|
||||
<string name="message_decoding_reset">Le décodeur vidéo de votre appareil continue de planter avec les paramètres de diffusion sélectionnés. Vos paramètres de diffusion ont été réinitialisés par défaut.</string>
|
||||
<string name="error_usb_prohibited">L\'accès USB est interdit par votre appareil. Vérifiez vos paramètres Knox ou MDM.</string>
|
||||
@@ -132,14 +132,14 @@
|
||||
<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>
|
||||
<string name="summary_checkbox_multi_controller">Lorsqu\'elle n\'est pas cochée, tous les contrôleurs sont regroupés</string>
|
||||
<string name="title_checkbox_vibrate_fallback">Emuler le support vibration</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">Vibre votre appareil pour émuler une vibration si votre manette ne le prend pas en charge</string>
|
||||
<string name="title_checkbox_vibrate_fallback">Emuler support vibration de secours</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">Emuler des tremblements si votre manette ne le prend pas en charge</string>
|
||||
<string name="title_seekbar_deadzone">Régler la zone morte du stick analogique</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Pilote de contrôleur Xbox 360/One</string>
|
||||
@@ -148,14 +148,14 @@
|
||||
<string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string>
|
||||
<string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string>
|
||||
<string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Activer les boutons de souris arrière et avant</string>
|
||||
<string name="summary_checkbox_mouse_nav_buttons">L\'activation de cette option peut entraîner un clic droit sur certains périphériques.</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Activer les boutons de la souris arrière et avant</string>
|
||||
<string name="summary_checkbox_mouse_nav_buttons">L\' activation de cette option peut entraîner un clic droit sur certains périphériques.</string>
|
||||
|
||||
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
|
||||
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
|
||||
<string name="title_checkbox_vibrate_osc">Activer les vibrations</string>
|
||||
<string name="summary_checkbox_vibrate_osc">Vibre votre appareil pour émuler les vibrations des commandes à l\'écran</string>
|
||||
<string name="summary_checkbox_vibrate_osc">Emuler des tremblements des commandes à l\'écran</string>
|
||||
<string name="title_only_l3r3">Montre seulement L3 et R3</string>
|
||||
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
|
||||
<string name="title_reset_osc">Effacer la disposition des commandes à l\'écran sauvegardée</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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
app/src/main/res/values-ro/arrays.xml
Normal file
20
app/src/main/res/values-ro/arrays.xml
Normal 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
app/src/main/res/values-ro/strings.xml
Normal file
191
app/src/main/res/values-ro/strings.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
14
app/src/main/res/values-uk/arrays.xml
Normal file
14
app/src/main/res/values-uk/arrays.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="decoder_names">
|
||||
<item>Автоматичний вибір декодера</item>
|
||||
<item>Примусове програмне декодування</item>
|
||||
<item>Примусове апаратне декодування</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="video_format_names">
|
||||
<item>Використовувати H.265 тільки якщо безпечно</item>
|
||||
<item>Завжди використовувати H.265 якщо доступно</item>
|
||||
<item>Ніколи не використовувати H.265</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
195
app/src/main/res/values-uk/strings.xml
Normal file
195
app/src/main/res/values-uk/strings.xml
Normal file
@@ -0,0 +1,195 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list">Подивитися список ігор</string>
|
||||
<string name="pcview_menu_pair_pc">Створити пару з ПК</string>
|
||||
<string name="pcview_menu_unpair_pc">Розірвати пару</string>
|
||||
<string name="pcview_menu_send_wol">Надіслати Wake-On-LAN запит</string>
|
||||
<string name="pcview_menu_delete_pc">Видалити ПК</string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing">Створення пари…</string>
|
||||
<string name="pair_pc_offline">Комп\'ютер вимкнений або знаходиться не в мережі</string>
|
||||
<string name="pair_pc_ingame">Комп\'ютер в даний момент знаходиться в грі. Ви повинні закрити гру перед створенням пари.</string>
|
||||
<string name="pair_pairing_title">Створення пари</string>
|
||||
<string name="pair_pairing_msg">Будь ласка, введіть цей PIN на ПК:</string>
|
||||
<string name="pair_incorrect_pin">Неправильний PIN</string>
|
||||
<string name="pair_fail">Створення пари не вдалося</string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online">Комп\'ютер в мережі</string>
|
||||
<string name="wol_no_mac">Неможливо розбудити ПК бо GFE не відправили MAC адреса</string>
|
||||
<string name="wol_waking_pc">Пробудження ПК…</string>
|
||||
<string name="wol_waking_msg">Пробудження ПК може зайняти кілька секунд.
|
||||
Якщо цього не відбувається, упевніться що Wake-On-LAN налаштований правильно.
|
||||
</string>
|
||||
<string name="wol_fail">Помилка при відправці Wake-On-LAN пакетів</string>
|
||||
|
||||
<!-- Unpair messages -->
|
||||
<string name="unpairing">Розрив пари…</string>
|
||||
<string name="unpair_success">Розрив пари закінчився успішно</string>
|
||||
<string name="unpair_fail">Розрив пари не вдався</string>
|
||||
<string name="unpair_error">Пристрій не було спарено</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline">Комп\'ютер вимкнений або знаходиться не в мережі</string>
|
||||
<string name="error_manager_not_running">Сервіс ComputerManager не запущено. Будь ласка, зачекайте кілька секунд або запустіть програму.</string>
|
||||
<string name="error_unknown_host">Неможливо знайти хост</string>
|
||||
<string name="error_404">GFE повернув помилку HTTP 404. Переконайтеся що Ваш ПК використовує підтримуваний GPU.
|
||||
Використання програм для віддаленого доступу також може викликати цю помилку. Спробуйте перезавантажити комп\'ютер або перевстановити GFE.
|
||||
</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">Створення з\'єднання</string>
|
||||
<string name="conn_establishing_msg">Підключення</string>
|
||||
<string name="conn_metered">Увага: Відбувається вимір Вашого мережевого з\'єднання!</string>
|
||||
<string name="conn_client_latency">Середня затримка декодування кадру:</string>
|
||||
<string name="conn_client_latency_hw">затримка апаратного декодування:</string>
|
||||
<string name="conn_hardware_latency">Середня затримка апаратного декодування:</string>
|
||||
<string name="conn_starting">Запуск</string>
|
||||
<string name="conn_error_title">Помилка з\'єднання</string>
|
||||
<string name="conn_error_msg">Запуск не вдався</string>
|
||||
<string name="conn_terminated_title">З\'єднання припинено</string>
|
||||
<string name="conn_terminated_msg">Підключення було перервано</string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">IP-адреса комп\'ютера з GeForce</string>
|
||||
<string name="searching_pc">Пошук комп\'ютерів із запущеним GameStream…\n\n
|
||||
Переконайтеся що GameStream увімкнений в налаштуваннях GeForce Experience в розділі SHIELD.</string>
|
||||
<string name="yes">Так</string>
|
||||
<string name="no">Ні</string>
|
||||
<string name="lost_connection">З\'єднання втрачено з ПК</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_menu_resume">Відновити сесію</string>
|
||||
<string name="applist_menu_quit">Вийти з сесії</string>
|
||||
<string name="applist_menu_quit_and_start">Вийти з поточної гри і запустити</string>
|
||||
<string name="applist_menu_cancel">Скасування</string>
|
||||
<string name="applist_menu_tv_channel">Додати на канал</string>
|
||||
<string name="applist_refresh_title">Список додатків</string>
|
||||
<string name="applist_refresh_msg">Оновлення додатків…</string>
|
||||
<string name="applist_refresh_error_title">Помилка</string>
|
||||
<string name="applist_refresh_error_msg">Помилка при отриманні списку додатків</string>
|
||||
<string name="applist_quit_app">Вихід з</string>
|
||||
<string name="applist_quit_success">Вихід відбувся успішно з</string>
|
||||
<string name="applist_quit_fail">Помилка при виході</string>
|
||||
<string name="applist_quit_confirmation">Ви впевнені, що хочете вийти з запущеного додатку? Усі незбережені дані будуть втрачені.</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc">Додавання ПК вручну</string>
|
||||
<string name="msg_add_pc">З\'єднання з ПК…</string>
|
||||
<string name="addpc_fail">Неможливо підключитися до вибраного комп\'ютера. Переконайтеся, що необхідні порти дозволені в налаштуваннях брандмауера.</string>
|
||||
<string name="addpc_success">Комп\'ютер доданий успішно</string>
|
||||
<string name="addpc_unknown_host">Неможливо знайти ПК за вказаною адресою. Переконайтеся, що Ви не зробили помилок під час його написання.</string>
|
||||
<string name="addpc_enter_ip">Ви повинні ввести IP адресу</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Загальні Налаштування</string>
|
||||
<string name="title_resolution_list">Виберіть розширення і частоту кадрів</string>
|
||||
<string name="summary_resolution_list">Вибір занадто високого значеня для свого пристрою може викликати гальма або вильоти</string>
|
||||
<string name="title_seekbar_bitrate">Виберіть бітрейт відео</string>
|
||||
<string name="summary_seekbar_bitrate">Низький бітрейт зменшить зависання. Збільшення бітрейта поліпшить якість зображення.</string>
|
||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
||||
<string name="title_checkbox_stretch_video">Розтягувати відео на весь екран</string>
|
||||
<string name="title_checkbox_disable_warnings">Відключити повідомлення з попередженнями</string>
|
||||
<string name="summary_checkbox_disable_warnings">Вимкнути екранні попередження про з\'єднання під час трансляції</string>
|
||||
|
||||
<string name="category_audio_settings">Аудіо Налаштування</string>
|
||||
<string name="title_audio_config_list">Конфігурація об\'ємного звуку</string>
|
||||
<string name="summary_audio_config_list">Увімкнути 5.1 або 7.1 об\'ємний звук для систем домашнього кінотеатру</string>
|
||||
|
||||
<string name="title_checkbox_multi_controller">Підтримка декількох контролерів</string>
|
||||
<string name="summary_checkbox_multi_controller">Коли вимкнено, всі контролери визначаються як один</string>
|
||||
<string name="title_seekbar_deadzone">Регулювати мертву зону аналогового стіку</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Драйвер контролерів Xbox 360/One</string>
|
||||
<string name="summary_checkbox_xb1_driver">Увімкнути вбудований USB драйвер для пристроїв без власної підтримки контролерів Xbox</string>
|
||||
|
||||
<string name="category_ui_settings">Налаштування інтерфейсу</string>
|
||||
<string name="title_language_list">Мова</string>
|
||||
<string name="summary_language_list">Мова, яка буде використовуватися в Moonlight</string>
|
||||
<string name="title_checkbox_small_icon_mode">Використовувати маленькі іконки</string>
|
||||
<string name="summary_checkbox_small_icon_mode">Використовувати маленькі іконки в сітці для відображення більшої кількості елементів на екрані</string>
|
||||
|
||||
<string name="category_host_settings">Налаштування Хоста</string>
|
||||
<string name="title_checkbox_enable_sops">Оптимізувати ігрові налаштування</string>
|
||||
<string name="summary_checkbox_enable_sops">Дозволити GFE змінювати налаштування ігор для оптимальної трансляції</string>
|
||||
<string name="title_checkbox_host_audio">Програвати звук на ПК</string>
|
||||
<string name="summary_checkbox_host_audio">Програвати звук на комп\'ютері і поточному пристрої</string>
|
||||
|
||||
<string name="category_advanced_settings">Розширені Налаштування</string>
|
||||
<string name="title_video_format">Змінити налаштування H.265</string>
|
||||
<string name="summary_video_format">H.265 знижує вимоги до пропускної здатності, але вимагає дуже нового пристрою</string>
|
||||
<string name="category_on_screen_controls_settings">Налаштування дисплею з кнопками</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">Показувати екранні кнопки</string>
|
||||
<string name="summary_checkbox_show_onscreen_controls">Відображати оверлей віртуального контролера на сенсорному екрані</string>
|
||||
<string name="title_only_l3r3">Показувати тільки L3 і R3</string>
|
||||
<string name="summary_only_l3r3">Приховувати всі екранні кнопки крім L3 і R3</string>
|
||||
<string name="scut_deleted_pc">ПК видалений</string>
|
||||
<string name="scut_not_paired">ПК не сполучений</string>
|
||||
<string name="help_loading_title">Перегляд Допомоги</string>
|
||||
<string name="help_loading_msg">Завантаження сторінки допомоги…</string>
|
||||
<string name="pair_already_in_progress">Сполучення вже в процесі</string>
|
||||
<string name="help">Допомога</string>
|
||||
<string name="applist_connect_msg">Підключення до ПК…</string>
|
||||
<string name="title_decoding_error">Збій відео декодера</string>
|
||||
<string name="message_decoding_error">Стався збій Moonlight через проблеми з відео декодером даного пристрою. Спробуйте змінити налаштування трансляції якщо збої будуть продовжуватися.</string>
|
||||
<string name="title_decoding_reset">Відео Налаштування Скинуті</string>
|
||||
<string name="message_decoding_reset">Відео декодер Вашого пристрою давав збої з вибраними налаштуваннями. Налаштування трансляції були скинуті до значень за замовчуванням.</string>
|
||||
<string name="error_usb_prohibited">USB доступ заборонений адміністратором пристрою. Перевірте налаштування Knox або MDM.</string>
|
||||
<string name="addpc_wrong_sitelocal">Адреса вказана невірно. Ви повинні ввести публічну IP-адресу Вашого роутера для передачі через інтернет.</string>
|
||||
<string name="title_checkbox_enable_pip">Увімкнути перегляд у режимі \"Картинка в картинці\"</string>
|
||||
<string name="summary_checkbox_enable_pip">Дозволяє переглядати трансляцію (але не керувати нею) під час роботи в інших додатках</string>
|
||||
<string name="title_checkbox_usb_bind_all">Відхилити підтримку контролерів Android</string>
|
||||
<string name="summary_checkbox_usb_bind_all">Змушує USB драйвер Moonlight взяти на себе роботу з усіма підтримуваними Xbox геймпадами</string>
|
||||
<string name="title_checkbox_mouse_emulation">Емуляція миші на геймпаді</string>
|
||||
<string name="summary_checkbox_mouse_emulation">Довге натиснення кнопки Start перемкне геймпад в режим миші</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_disable_frame_drop">Ніколи не пропускати кадри</string>
|
||||
<string name="summary_disable_frame_drop">Може зменшити мікрозависання на деяких пристроях, але також збільшити затримку</string>
|
||||
<string name="title_enable_hdr">Увімкнути HDR (Експериментально)</string>
|
||||
<string name="summary_enable_hdr">Транслювати в HDR якщо гра і GPU комп\'ютера підтримують це. HDR вимагає відеокарти GTX 1000 серії або більш нової.</string>
|
||||
|
||||
<string name="title_checkbox_vibrate_osc">Увімкнути вібрацію</string>
|
||||
<string name="title_fps_list">Частота кадрів</string>
|
||||
<string name="applist_menu_details">Деталі</string>
|
||||
<string name="applist_menu_scut">Створити ярлик</string>
|
||||
<string name="category_input_settings">Налаштування введення</string>
|
||||
<string name="title_checkbox_touchscreen_trackpad">Використовувати сенсорний екран як трекпад</string>
|
||||
<string name="summary_checkbox_touchscreen_trackpad">Якщо увімкнено, сенсорний екран працює як трекпад. Якщо його вимкнено, сенсорний екран безпосередньо керує курсором миші.</string>
|
||||
<string name="delete_pc_msg">Ви впевнені що хочете видалити цей ПК?</string>
|
||||
<string name="pcview_menu_details">Деталі</string>
|
||||
<string name="poor_connection_msg">Слабке з\'єднання з ПК</string>
|
||||
<string name="title_details">Деталі</string>
|
||||
<string name="title_enable_perf_overlay">Увімкнути відображення статистики</string>
|
||||
<string name="title_unlock_fps">Розблокувати всі можливі частоти оновлення</string>
|
||||
<string name="applist_details_id">ID додатку:</string>
|
||||
<string name="title_checkbox_vibrate_fallback">Емуляція вібровіддачі</string>
|
||||
<string name="summary_checkbox_vibrate_osc">Вібрація пристрою для емуляції вібровіддачі при екранному управлінні</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">Вібрувати пристрій для емуляції вібровіддачі для геймпадов без підтримки вібрації</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="scut_pc_not_found">ПК не знайдено</string>
|
||||
<string name="unable_to_pin_shortcut">Поточний лаунчер не дозволяє створювати закріплені ярлики</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Увімкнути кнопки вперед і назад для миші</string>
|
||||
<string name="slow_connection_msg">Повільне підключення до ПК\nЗменшити бітрейт</string>
|
||||
<string name="summary_unlock_fps">Трансляція зі швидкістю 90 або 120 кадрів в секунду може зменшити затримку на пристроях високого класу, але може викликати затримки або збій на пристроях без підтримки цього функціоналу</string>
|
||||
<string name="summary_enable_perf_overlay">Відображення накладення на екрані з інформацією про продуктивність під час трансляції в режимі реального часу</string>
|
||||
<string name="perf_overlay_text">Розширення відео: %1$s\nДекодер: %2$s\nРозрахункова частота кадрів ПК-хоста: %3$.2f FPS\nВхідна частота кадрів з мережі: %4$.2f FPS\nЧастота кадрів під час рендерінгу: %5$.2f FPS\nВідкинутих кадрів вашою мережею: %6$.2f%%\nСередній час отримання: %7$.2f ms\nСередній час декодування: %8$.2f ms</string>
|
||||
<string name="summary_fps_list">Збільшення для більш плавного відео потоку. Зменшіть для кращої продуктивності на більш слабких пристроях.</string>
|
||||
<string name="scut_invalid_uuid">Зазначений ПК недійсний</string>
|
||||
<string name="scut_invalid_app_id">Зазначений додаток недійсне</string>
|
||||
<string name="title_osc_opacity">Змінити непрозорість екранних елементів керування</string>
|
||||
<string name="summary_osc_opacity">Зробити екранні елементи керування більш/менш прозорими</string>
|
||||
<string name="dialog_title_osc_opacity">Зміна непрозорості</string>
|
||||
<string name="suffix_osc_opacity">%</string>
|
||||
<string name="title_enable_post_stream_toast">Показувати затримку після трансляції</string>
|
||||
<string name="summary_enable_post_stream_toast">Вивести інформаційне повідомлення про затримку після закінчення потоку</string>
|
||||
|
||||
</resources>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Shortcut strings -->
|
||||
<string name="scut_deleted_pc"> 电脑已删除 </string>
|
||||
<string name="scut_not_paired"> 电脑没有配对成功 </string>
|
||||
<string name="scut_not_paired"> 电脑未配对 </string>
|
||||
<string name="scut_pc_not_found"> 电脑未找到 </string>
|
||||
<string name="scut_invalid_uuid"> 提供的电脑无效 </string>
|
||||
<string name="scut_invalid_app_id"> 提供的App无效 </string>
|
||||
|
||||
<!-- Help strings -->
|
||||
<string name="help_loading_title"> 帮助查看器 </string>
|
||||
<string name="help_loading_msg"> 加载帮助页面中…… </string>
|
||||
<string name="help_loading_title"> 查看帮助 </string>
|
||||
<string name="help_loading_msg"> 正在加载帮助页面…… </string>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list"> 浏览游戏列表 </string>
|
||||
@@ -14,23 +18,24 @@
|
||||
<string name="pcview_menu_unpair_pc"> 取消配对 </string>
|
||||
<string name="pcview_menu_send_wol"> 发送 Wake-On-LAN 请求 </string>
|
||||
<string name="pcview_menu_delete_pc"> 删除电脑 </string>
|
||||
<string name="pcview_menu_details"> 查看详情 </string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing"> 配对中…… </string>
|
||||
<string name="pair_pc_offline"> 电脑离线中 </string>
|
||||
<string name="pair_pc_ingame"> 电脑正在游戏中,你必须在配对之前先退出游戏 </string>
|
||||
<string name="pair_pc_ingame"> 电脑正在游戏中,在配对之前你必须先退出游戏 </string>
|
||||
<string name="pair_pairing_title"> 配对中 </string>
|
||||
<string name="pair_pairing_msg"> 请在目标电脑上输入以下的PIN码: </string>
|
||||
<string name="pair_pairing_msg"> 请在目标电脑上输入以下PIN码: </string>
|
||||
<string name="pair_incorrect_pin"> PIN码错误 </string>
|
||||
<string name="pair_fail"> 配对失败 </string>
|
||||
<string name="pair_already_in_progress"> 已经在配对中,请稍等 </string>
|
||||
<string name="pair_already_in_progress"> 配对中,请稍候 </string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online"> 电脑在线中 </string>
|
||||
<string name="wol_no_mac"> 由于GeForce Experience没有发送MAC地址过来因此无法唤醒电脑 </string>
|
||||
<string name="wol_no_mac"> 无法唤醒电脑因为GFE没有返回MAC地址 </string>
|
||||
<string name="wol_waking_pc"> 唤醒电脑中…… </string>
|
||||
<string name="wol_waking_msg"> 远程唤醒电脑需要一些时间\n\n
|
||||
如果电脑没有唤醒,请确保Wake-On-LAN的设置无误
|
||||
<string name="wol_waking_msg"> 唤醒电脑需要一些时间\n
|
||||
如果电脑没有唤醒,请确保Wake-On-LAN设置无误
|
||||
</string>
|
||||
<string name="wol_fail"> 无法发送Wake-On-LAN数据包 </string>
|
||||
|
||||
@@ -42,40 +47,54 @@
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline"> 电脑离线中 </string>
|
||||
<string name="error_manager_not_running">ComputerManager服务不在运行中\n\n请稍等几秒或者直接重启Moonlight</string>
|
||||
<string name="error_unknown_host"> 无法解析电脑地址 </string>
|
||||
<string name="error_404"> GeForce Experience返回了HTTP 404 错误。确保你的显卡支持GAMESTREAM\n\n
|
||||
使用远程控制软件同样会引起此错误,请尝试重启电脑或者重新安装GeForce Experience
|
||||
<string name="error_manager_not_running"> ComputerManager服务未运行。\n请稍等几秒或重启App </string>
|
||||
<string name="error_unknown_host"> 无法解析主机地址 </string>
|
||||
<string name="error_404"> GFE返回了HTTP 404 错误,确保你的电脑显卡支持串流。\n
|
||||
使用远程桌面软件同样会引起此错误,请尝试重启电脑或重装GFE
|
||||
</string>
|
||||
<string name="title_decoding_error"> 视频解码器崩溃 </string>
|
||||
<string name="message_decoding_error"> 由于与该设备的视频解码器不兼容,Moonlight已崩溃。确保你电脑上的GFE已更新至最新版本,如果崩溃继续,请尝试调整串流设置。 </string>
|
||||
<string name="title_decoding_reset"> 重置视频设置 </string>
|
||||
<string name="message_decoding_reset"> 由于设备的视频解码器在你选择的串流设置上持续崩溃,已重置你的串流设置。 </string>
|
||||
<string name="error_usb_prohibited"> 设备管理员已禁止USB访问。请检查您的Knox或MDM设置。 </string>
|
||||
<string name="unable_to_pin_shortcut"> 您当前的桌面启动器不允许创建长按子菜单。 </string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title"> 建立连接中 </string>
|
||||
<string name="conn_establishing_msg"> 启动连接中 </string>
|
||||
<string name="conn_metered"> 警告: 你正在使用移动网络,继续使用将会产生大量流量费用! </string>
|
||||
<string name="conn_client_latency"> 每帧解码平均延迟:</string>
|
||||
<string name="conn_client_latency_hw"> 硬解码器延迟:</string>
|
||||
<string name="conn_hardware_latency"> 硬解码器平均延迟:</string>
|
||||
<string name="conn_metered"> 警告:你正在使用移动网络连接! </string>
|
||||
<string name="conn_client_latency"> 平均每帧解码延迟:</string>
|
||||
<string name="conn_client_latency_hw"> 硬件解码器延迟:</string>
|
||||
<string name="conn_hardware_latency"> 硬件解码器平均延迟:</string>
|
||||
<string name="conn_starting"> 启动中…… </string>
|
||||
<string name="conn_error_title"> 连接错误 </string>
|
||||
<string name="conn_error_msg"> 启动失败 </string>
|
||||
<string name="conn_terminated_title"> 连接被终结 </string>
|
||||
<string name="conn_terminated_title"> 连接终结 </string>
|
||||
<string name="conn_terminated_msg"> 连接已被终结 </string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint"> 串流电脑的IP地址 </string>
|
||||
<string name="searching_pc"> 正在搜寻运行着GAMESTREAM的电脑…… \n\n
|
||||
确保GeForce Experience里面SHIELD选项里面的GAMESTREAM开关是开着的 </string>
|
||||
<string name="yes"> 是 </string>
|
||||
<string name="no"> 否 </string>
|
||||
<string name="lost_connection"> 失去了与电脑的连接 </string>
|
||||
<string name="help">帮助</string>
|
||||
<string name="searching_pc"> 正在搜寻运行GAMESTREAM的电脑…… \n
|
||||
请确保GFE SHIELD设置里的GAMESTREAM已开启。 </string>
|
||||
<string name="yes"> 确定 </string>
|
||||
<string name="no"> 取消 </string>
|
||||
<string name="lost_connection"> 与电脑失去连接 </string>
|
||||
<string name="title_details"> 详情 </string>
|
||||
<string name="help"> 帮助 </string>
|
||||
<string name="delete_pc_msg"> 你确定要删除这台电脑? </string>
|
||||
<string name="slow_connection_msg"> 与电脑连接过慢 \n 请降低码率 </string>
|
||||
<string name="poor_connection_msg"> 与电脑连接不良 </string>
|
||||
<string name="perf_overlay_text"> 视频分辨率: %1$s\n解码器: %2$s\n估计主机帧数: %3$.2f FPS\n网络接收帧数: %4$.2f FPS\n渲染帧数: %5$.2f FPS\n网络丢失帧: %6$.2f%%\n平均接收时间: %7$.2f ms\n平均解码时间: %8$.2f ms </string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_connect_msg"> 连接到电脑中…… </string>
|
||||
<string name="applist_connect_msg"> 正在连接电脑…… </string>
|
||||
<string name="applist_menu_resume"> 恢复串流 </string>
|
||||
<string name="applist_menu_quit"> 退出串流 </string>
|
||||
<string name="applist_menu_quit_and_start"> 退出当前游戏并开始这个游戏 </string>
|
||||
<string name="applist_menu_cancel"> 取消 </string>
|
||||
<string name="applist_menu_details"> 查看详情 </string>
|
||||
<string name="applist_menu_scut"> 创建快捷方式 </string>
|
||||
<string name="applist_menu_tv_channel"> 添加到频道 </string>
|
||||
<string name="applist_refresh_title"> 游戏列表 </string>
|
||||
<string name="applist_refresh_msg"> 刷新中…… </string>
|
||||
<string name="applist_refresh_error_title"> 错误 </string>
|
||||
@@ -83,81 +102,99 @@
|
||||
<string name="applist_quit_app"> 退出中 </string>
|
||||
<string name="applist_quit_success"> 成功退出串流 </string>
|
||||
<string name="applist_quit_fail"> 退出串流失败 </string>
|
||||
<string name="applist_quit_confirmation"> 您确定要退出当前游戏?\n\n所有未保存的数据将丢失 </string>
|
||||
<string name="applist_quit_confirmation"> 您确定要退出当前游戏?\n所有未保存的数据将丢失。 </string>
|
||||
<string name="applist_details_id">App ID:</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc"> 手动添加电脑 </string>
|
||||
<string name="msg_add_pc"> 连接到电脑中…… </string>
|
||||
<string name="addpc_fail"> 无法连接到指定的电脑。请确保指定的端口没有被防火墙阻止 </string>
|
||||
<string name="msg_add_pc"> 正在连接电脑…… </string>
|
||||
<string name="addpc_fail"> 无法连接至指定电脑。请确保所需端口没有被防火墙阻止 </string>
|
||||
<string name="addpc_success"> 成功添加电脑 </string>
|
||||
<string name="addpc_unknown_host"> 无法解析电脑的IP地址,请确保IP地址输入无误 </string>
|
||||
<string name="addpc_enter_ip"> 请输入一个IP地址! </string>
|
||||
<string name="addpc_wrong_sitelocal"> 该地址似乎不正确。 您必须使用路由器的公共IP地址通过Internet进行串流。 </string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings"> 基本设置 </string>
|
||||
<string name="title_resolution_list"> 选择目标分辨率和帧数 </string>
|
||||
<string name="summary_resolution_list"> 过高的设定会引起串流卡顿甚至软件闪退 </string>
|
||||
<string name="title_seekbar_bitrate"> 选择目标视频码率 </string>
|
||||
<string name="summary_seekbar_bitrate"> 低码率减少卡顿,高码率提高画质 </string>
|
||||
<string name="title_resolution_list"> 视频分辨率 </string>
|
||||
<string name="summary_resolution_list"> 高分辨率提升图像清晰度。 \n 低分辨率提升在低端设备和较慢网络中的串流体验。 </string>
|
||||
<string name="title_fps_list"> 视频帧数 </string>
|
||||
<string name="summary_fps_list"> 高帧数提升视频流流畅度。 \n 低帧数提升在低端设备中的串流体验。</string>
|
||||
<string name="title_seekbar_bitrate"> 视频码率 </string>
|
||||
<string name="summary_seekbar_bitrate"> 高码率提升图像质量。 \n 低码率提升在较慢网络中的串流体验。 </string>
|
||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
||||
<string name="title_unlock_fps"> 解锁所有可用帧数 </string>
|
||||
<string name="summary_unlock_fps"> 以90或120帧串流可能会减少在高端设备上的网络延迟,但会在不支持的设备上造成卡顿或崩溃。 </string>
|
||||
<string name="title_checkbox_stretch_video"> 将画面拉伸至全屏 </string>
|
||||
<string name="title_checkbox_disable_warnings"> 禁用错误提示 </string>
|
||||
<string name="summary_checkbox_disable_warnings"> 串流过程中禁用连接错误提示 </string>
|
||||
<string name="summary_checkbox_disable_warnings"> 串流中禁用连接错误提示 </string>
|
||||
<string name="title_checkbox_enable_pip">启用画中画观察模式</string>
|
||||
<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\n需要GeForce Experience 2.7 或更高版本 </string>
|
||||
<string name="title_audio_config_list"> 环绕声设置 </string>
|
||||
<string name="summary_audio_config_list"> 为家庭影院系统启用5.1或7.1环绕声 </string>
|
||||
|
||||
<string name="title_checkbox_multi_controller"> 启用多手柄支持 </string>
|
||||
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄将会认作一个手柄 </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>
|
||||
<string name="summary_checkbox_vibrate_fallback"> 如果你的手柄不支持震动,则震动设备以模拟游戏震动效果 </string>
|
||||
<string name="title_seekbar_deadzone"> 调整摇杆死区 </string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Xbox 360/One 手柄驱动 </string>
|
||||
<string name="summary_checkbox_xb1_driver"> 若要在那些没有原生Xbox手柄驱动的设备上使用Xbox手柄,请勾上此复选框 </string>
|
||||
<string name="summary_checkbox_xb1_driver"> 为缺少原生Xbox手柄支持的设备启用内置USB驱动 </string>
|
||||
<string name="title_checkbox_usb_bind_all"> 覆盖安卓手柄支持 </string>
|
||||
<string name="summary_checkbox_usb_bind_all"> 强制Moonlight的USB驱动接管所有受支持的Xbox手柄 </string>
|
||||
<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="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>
|
||||
<string name="summary_checkbox_show_onscreen_controls"> 将在串流画面上显示一层虚拟手柄 </string>
|
||||
<string name="category_on_screen_controls_settings"> 屏幕控制按钮设置 </string>
|
||||
<string name="title_checkbox_show_onscreen_controls"> 显示屏幕控制按钮 </string>
|
||||
<string name="summary_checkbox_show_onscreen_controls"> 在触摸屏上显示一层虚拟手柄 </string>
|
||||
<string name="title_checkbox_vibrate_osc"> 启用震动 </string>
|
||||
<string name="summary_checkbox_vibrate_osc"> 使用屏幕控制按钮时震动设备以模拟游戏震动效果 </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="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_language_list"> 语言 </string>
|
||||
<string name="summary_language_list"> 选择Moonlight显示的语言 </string>
|
||||
<string name="title_checkbox_small_icon_mode"> 使用小图标 </string>
|
||||
<string name="summary_checkbox_small_icon_mode"> 使用小图标以在屏幕上显示更多项目 </string>
|
||||
|
||||
<string name="category_host_settings"> 主机设置 </string>
|
||||
<string name="title_checkbox_enable_sops"> 优化游戏设置 </string>
|
||||
<string name="summary_checkbox_enable_sops"> 允许GeForce Experience为最佳串流效果自动更改游戏设置 </string>
|
||||
<string name="title_checkbox_host_audio"> 将声音输出到电脑上 </string>
|
||||
<string name="summary_checkbox_host_audio"> 将在电脑和本设备同时输出声音 </string>
|
||||
<string name="summary_checkbox_enable_sops"> 允许GFE为最佳串流效果自动更改游戏设置 </string>
|
||||
<string name="title_checkbox_host_audio"> 在电脑上播放声音 </string>
|
||||
<string name="summary_checkbox_host_audio"> 在电脑和本设备同时输出声音 </string>
|
||||
|
||||
<string name="category_advanced_settings"> 高级设置 </string>
|
||||
<string name="title_video_format"> H.265设置 </string>
|
||||
<string name="summary_video_format">H.265能降低带宽需求,但是需要设备支持 </string>
|
||||
<string name="applist_menu_scut">创建快捷方式</string>
|
||||
<string name="category_input_settings">输入设置</string>
|
||||
<string name="applist_menu_details">查看详情</string>
|
||||
<string name="dialog_text_reset_osc">你确定要删除所保存的按钮布局吗?</string>
|
||||
<string name="dialog_title_reset_osc">重置按钮布局</string>
|
||||
<string name="delete_pc_msg">你确定要删除这台电脑?</string>
|
||||
<string name="pcview_menu_details">查看详情</string>
|
||||
<string name="scut_pc_not_found">电脑无法找到</string>
|
||||
<string name="toast_reset_osc_success">按钮布局已经重置</string>
|
||||
<string name="title_fps_list">视频帧数</string>
|
||||
<string name="title_unlock_fps">解锁所有可用帧数</string>
|
||||
<string name="title_only_l3r3">只显示[L3]和[R3]</string>
|
||||
<string name="title_reset_osc">重置已经保存的触摸按钮布局</string>
|
||||
<string name="title_disable_frame_drop">永不掉帧</string>
|
||||
<string name="title_enable_hdr">启用 HDR (实验)</string>
|
||||
<string name="title_checkbox_vibrate_osc">启动震动</string>
|
||||
<string name="title_details">详情</string>
|
||||
<string name="title_decoding_reset">重置视频设置</string>
|
||||
<string name="title_checkbox_mouse_emulation">通过手柄模拟鼠标</string>
|
||||
<string name="summary_only_l3r3">隐藏所有虚拟按钮除了L3和R3</string>
|
||||
<string name="title_enable_perf_overlay">启用性能信息</string>
|
||||
<string name="summary_enable_perf_overlay">在串流中显示实时性能信息</string>
|
||||
<string name="perf_overlay_text">视频分辨率: %1$s\n解码器: %2$s\n估计主机帧数: %3$.2f FPS\n网络接收帧数: %4$.2f FPS\n渲染帧数: %5$.2f FPS\n网络丢失帧: %6$.2f%%\n平均接收时间: %7$.2f ms\n平均解码时间: %8$.2f ms</string>
|
||||
<string name="title_disable_frame_drop"> 永不掉帧 </string>
|
||||
<string name="summary_disable_frame_drop"> 可能会减少在一些设备上的卡顿,但会增加延迟 </string>
|
||||
<string name="title_video_format"> 更改H.265设置 </string>
|
||||
<string name="summary_video_format">H.265能降低视频带宽需求,但需要较新的设备才能支持</string>
|
||||
<string name="title_enable_hdr"> 启用 HDR (实验) </string>
|
||||
<string name="summary_enable_hdr"> 当游戏和显卡支持时以HDR模式串流。 HDR需要GTX 1000系列或更高规格显卡。 </string>
|
||||
<string name="title_enable_perf_overlay"> 启用性能信息 </string>
|
||||
<string name="summary_enable_perf_overlay"> 在串流中显示实时性能信息 </string>
|
||||
<string name="title_enable_post_stream_toast">串流完毕显示延迟信息</string>
|
||||
<string name="summary_enable_post_stream_toast">串流结束后显示延迟信息</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,38 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Shortcut strings -->
|
||||
<string name="scut_deleted_pc"> 電腦已刪除 </string>
|
||||
<string name="scut_not_paired"> 電腦沒有配對成功 </string>
|
||||
<string name="scut_not_paired"> 電腦未配對 </string>
|
||||
<string name="scut_pc_not_found"> 電腦未找到 </string>
|
||||
<string name="scut_invalid_uuid"> 提供的電腦無效 </string>
|
||||
<string name="scut_invalid_app_id"> 提供的App無效 </string>
|
||||
|
||||
<!-- Help strings -->
|
||||
<string name="help_loading_title"> 幫助查看器 </string>
|
||||
<string name="help_loading_msg"> 載入幫助頁面中…… </string>
|
||||
<string name="help_loading_title"> 查看幫助 </string>
|
||||
<string name="help_loading_msg"> 正在載入説明頁面…… </string>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list"> 瀏覽遊戲列表 </string>
|
||||
<string name="pcview_menu_app_list"> 流覽遊戲列表 </string>
|
||||
<string name="pcview_menu_pair_pc"> 和電腦配對 </string>
|
||||
<string name="pcview_menu_unpair_pc"> 取消配對 </string>
|
||||
<string name="pcview_menu_send_wol"> 發送 Wake-On-LAN 請求 </string>
|
||||
<string name="pcview_menu_delete_pc"> 刪除電腦 </string>
|
||||
<string name="pcview_menu_details"> 查看詳情 </string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing"> 配對中…… </string>
|
||||
<string name="pair_pc_offline"> 電腦離線中 </string>
|
||||
<string name="pair_pc_ingame"> 電腦正在遊戲中,你必須在配對之前先退出遊戲 </string>
|
||||
<string name="pair_pc_ingame"> 電腦正在遊戲中,在配對之前你必須先退出遊戲 </string>
|
||||
<string name="pair_pairing_title"> 配對中 </string>
|
||||
<string name="pair_pairing_msg"> 請在目標電腦上輸入以下的PIN碼: </string>
|
||||
<string name="pair_pairing_msg"> 請在目標電腦上輸入以下PIN碼: </string>
|
||||
<string name="pair_incorrect_pin"> PIN碼錯誤 </string>
|
||||
<string name="pair_fail"> 配對失敗 </string>
|
||||
<string name="pair_already_in_progress"> 已經在配對中,請稍等 </string>
|
||||
<string name="pair_already_in_progress"> 配對中,請稍候 </string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online"> 電腦在線中 </string>
|
||||
<string name="wol_no_mac"> 由於GeForce Experience沒有發送MAC地址過來因此無法喚醒電腦 </string>
|
||||
<string name="wol_pc_online"> 電腦線上中 </string>
|
||||
<string name="wol_no_mac"> 無法喚醒電腦因為GFE沒有返回MAC地址 </string>
|
||||
<string name="wol_waking_pc"> 喚醒電腦中…… </string>
|
||||
<string name="wol_waking_msg"> 遠程喚醒電腦需要一些時間\n\n
|
||||
如果電腦沒有喚醒,請確保Wake-On-LAN的設置無誤
|
||||
<string name="wol_waking_msg"> 喚醒電腦需要一些時間\n
|
||||
如果電腦沒有喚醒,請確保Wake-On-LAN設置無誤
|
||||
</string>
|
||||
<string name="wol_fail"> 無法發送Wake-On-LAN數據包 </string>
|
||||
<string name="wol_fail"> 無法發送Wake-On-LAN資料包 </string>
|
||||
|
||||
<!-- Unpair messages -->
|
||||
<string name="unpairing"> 取消配對中…… </string>
|
||||
@@ -42,40 +47,54 @@
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline"> 電腦離線中 </string>
|
||||
<string name="error_manager_not_running">ComputerManager服務不在運行中\n\n請稍等幾秒或者直接重啟Moonlight</string>
|
||||
<string name="error_unknown_host"> 無法解析電腦地址 </string>
|
||||
<string name="error_404"> GeForce Experience返回了HTTP 404 錯誤。確保你的顯卡支持GAMESTREAM\n\n
|
||||
使用遠程控制軟體同樣會引起此錯誤,請嘗試重啟電腦或者重新安裝GeForce Experience
|
||||
<string name="error_manager_not_running"> ComputerManager服務未運行。\n請稍等幾秒或重啟App </string>
|
||||
<string name="error_unknown_host"> 無法解析主機位址 </string>
|
||||
<string name="error_404"> GFE返回了HTTP 404 錯誤,確保你的電腦顯卡支援串流。\n
|
||||
使用遠端桌面軟體同樣會引起此錯誤,請嘗試重啟電腦或重裝GFE
|
||||
</string>
|
||||
<string name="title_decoding_error"> 視頻解碼器崩潰 </string>
|
||||
<string name="message_decoding_error"> 由於與該設備的視頻解碼器不相容,Moonlight已崩潰。確保你電腦上的GFE已更新至最新版本,如果崩潰繼續,請嘗試調整串流設置。 </string>
|
||||
<string name="title_decoding_reset"> 重置視頻設置 </string>
|
||||
<string name="message_decoding_reset"> 由於設備的視頻解碼器在你選擇的串流設置上持續崩潰,已重置你的串流設置。 </string>
|
||||
<string name="error_usb_prohibited"> 設備管理員已禁止USB訪問。請檢查您的Knox或MDM設置。 </string>
|
||||
<string name="unable_to_pin_shortcut"> 您當前的桌面啟動器不允許創建長按子功能表。 </string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title"> 建立連接中 </string>
|
||||
<string name="conn_establishing_msg"> 啟動連接中 </string>
|
||||
<string name="conn_metered"> 警告: 你正在使用移動網路,繼續使用將會產生大量流量費用! </string>
|
||||
<string name="conn_client_latency"> 每幀解碼平均延遲:</string>
|
||||
<string name="conn_client_latency_hw"> 硬解碼器延遲:</string>
|
||||
<string name="conn_hardware_latency"> 硬解碼器平均延遲:</string>
|
||||
<string name="conn_metered"> 警告:你正在使用移動網路連接! </string>
|
||||
<string name="conn_client_latency"> 平均每幀解碼延遲:</string>
|
||||
<string name="conn_client_latency_hw"> 硬體解碼器延遲:</string>
|
||||
<string name="conn_hardware_latency"> 硬體解碼器平均延遲:</string>
|
||||
<string name="conn_starting"> 啟動中…… </string>
|
||||
<string name="conn_error_title"> 連接錯誤 </string>
|
||||
<string name="conn_error_msg"> 啟動失敗 </string>
|
||||
<string name="conn_terminated_title"> 連接被終結 </string>
|
||||
<string name="conn_terminated_title"> 連接終結 </string>
|
||||
<string name="conn_terminated_msg"> 連接已被終結 </string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint"> 串流電腦的IP地址 </string>
|
||||
<string name="searching_pc"> 正在搜尋運行著GAMESTREAM的電腦…… \n\n
|
||||
確保GeForce Experience裡面SHIELD選項裡面的GAMESTREAM開關是開著的 </string>
|
||||
<string name="yes"> 是 </string>
|
||||
<string name="no"> 否 </string>
|
||||
<string name="lost_connection"> 失去了與電腦的連接 </string>
|
||||
<string name="help">幫助</string>
|
||||
<string name="searching_pc"> 正在搜尋運行GAMESTREAM的電腦…… \n
|
||||
請確保GFE SHIELD設置裡的GAMESTREAM已開啟。 </string>
|
||||
<string name="yes"> 確定 </string>
|
||||
<string name="no"> 取消 </string>
|
||||
<string name="lost_connection"> 與電腦失去連接 </string>
|
||||
<string name="title_details"> 詳情 </string>
|
||||
<string name="help"> 幫助 </string>
|
||||
<string name="delete_pc_msg"> 你確定要刪除這台電腦? </string>
|
||||
<string name="slow_connection_msg"> 與電腦連接過慢 \n 請降低碼率 </string>
|
||||
<string name="poor_connection_msg"> 與電腦連接不良 </string>
|
||||
<string name="perf_overlay_text"> 視頻解析度: %1$s\n解碼器: %2$s\n估計主機幀數: %3$.2f FPS\n網路接收幀數: %4$.2f FPS\n渲染幀數: %5$.2f FPS\n網路丟失幀: %6$.2f%%\n平均接收時間: %7$.2f ms\n平均解碼時間: %8$.2f ms </string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_connect_msg"> 連接到電腦中…… </string>
|
||||
<string name="applist_connect_msg"> 正在連接電腦…… </string>
|
||||
<string name="applist_menu_resume"> 恢復串流 </string>
|
||||
<string name="applist_menu_quit"> 退出串流 </string>
|
||||
<string name="applist_menu_quit_and_start"> 退出當前遊戲並開始這個遊戲 </string>
|
||||
<string name="applist_menu_cancel"> 取消 </string>
|
||||
<string name="applist_menu_details"> 查看詳情 </string>
|
||||
<string name="applist_menu_scut"> 創建快捷方式 </string>
|
||||
<string name="applist_menu_tv_channel"> 添加到頻道 </string>
|
||||
<string name="applist_refresh_title"> 遊戲列表 </string>
|
||||
<string name="applist_refresh_msg"> 刷新中…… </string>
|
||||
<string name="applist_refresh_error_title"> 錯誤 </string>
|
||||
@@ -83,85 +102,99 @@
|
||||
<string name="applist_quit_app"> 退出中 </string>
|
||||
<string name="applist_quit_success"> 成功退出串流 </string>
|
||||
<string name="applist_quit_fail"> 退出串流失敗 </string>
|
||||
<string name="applist_quit_confirmation"> 您確定要退出當前遊戲?\n\n所有未保存的數據將丟失 </string>
|
||||
<string name="applist_quit_confirmation"> 您確定要退出當前遊戲?\n所有未保存的資料將丟失。 </string>
|
||||
<string name="applist_details_id">App ID:</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc"> 手動添加電腦 </string>
|
||||
<string name="msg_add_pc"> 連接到電腦中…… </string>
|
||||
<string name="addpc_fail"> 無法連接到指定的電腦。請確保指定的埠沒有被防火牆阻止 </string>
|
||||
<string name="msg_add_pc"> 正在連接電腦…… </string>
|
||||
<string name="addpc_fail"> 無法連接至指定電腦。請確保所需埠沒有被防火牆阻止 </string>
|
||||
<string name="addpc_success"> 成功添加電腦 </string>
|
||||
<string name="addpc_unknown_host"> 無法解析電腦的IP地址,請確保IP地址輸入無誤 </string>
|
||||
<string name="addpc_enter_ip"> 請輸入一個IP地址! </string>
|
||||
<string name="addpc_unknown_host"> 無法解析電腦的IP位址,請確保IP位址輸入無誤 </string>
|
||||
<string name="addpc_enter_ip"> 請輸入一個IP位址! </string>
|
||||
<string name="addpc_wrong_sitelocal"> 該位址似乎不正確。 您必須使用路由器的公共IP位址通過Internet進行串流。 </string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings"> 基本設置 </string>
|
||||
<string name="title_resolution_list"> 選擇目標解析度和幀數 </string>
|
||||
<string name="summary_resolution_list"> 過高的設定會引起串流卡頓甚至軟體閃退 </string>
|
||||
<string name="title_fps_list">影像幀數</string>
|
||||
<string name="summary_fps_list">增加以提供更流暢的影像串流. 減少以在較低的配備上獲得較好的效能.</string>
|
||||
<string name="title_seekbar_bitrate"> 選擇目標影像碼率 </string>
|
||||
<string name="summary_seekbar_bitrate"> 低碼率減少卡頓,高碼率提高畫質 </string>
|
||||
<string name="title_resolution_list"> 視頻解析度 </string>
|
||||
<string name="summary_resolution_list"> 高解析度提升圖像清晰度。 \n 低解析度提升在低端設備和較慢網路中的串流體驗。 </string>
|
||||
<string name="title_fps_list"> 視頻幀數 </string>
|
||||
<string name="summary_fps_list"> 高幀數提升視頻流流暢度。 \n 低幀數提升在低端設備中的串流體驗。</string>
|
||||
<string name="title_seekbar_bitrate"> 視頻碼率 </string>
|
||||
<string name="summary_seekbar_bitrate"> 高碼率提升圖像品質。 \n 低碼率提升在較慢網路中的串流體驗。 </string>
|
||||
<string name="suffix_seekbar_bitrate">Kbps</string>
|
||||
<string name="title_unlock_fps">解鎖所有可使用的幀率</string>
|
||||
<string name="summary_unlock_fps">串流於90或120幀下可以減少高級配備的延遲, 但可能導致無法支持的配備出現延遲或崩潰</string>
|
||||
<string name="title_unlock_fps"> 解鎖所有可用幀數 </string>
|
||||
<string name="summary_unlock_fps"> 以90或120幀串流可能會減少在高端設備上的網路延遲,但會在不支援的設備上造成卡頓或崩潰。 </string>
|
||||
<string name="title_checkbox_stretch_video"> 將畫面拉伸至全屏 </string>
|
||||
<string name="title_checkbox_disable_warnings"> 禁用錯誤提示 </string>
|
||||
<string name="summary_checkbox_disable_warnings"> 串流過程中禁用連接錯誤提示 </string>
|
||||
<string name="title_checkbox_enable_pip">啟用子母畫面</string>
|
||||
<string name="summary_checkbox_enable_pip">當多工處理時, 允許觀看串流畫面(無法控制)</string>
|
||||
<string name="summary_checkbox_disable_warnings"> 串流中禁用連接錯誤提示 </string>
|
||||
<string name="title_checkbox_enable_pip">啟用畫中畫觀察模式</string>
|
||||
<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\n需要GeForce Experience 2.7 或更高版本 </string>
|
||||
<string name="category_audio_settings"> 音訊設置 </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="title_checkbox_multi_controller"> 啟用多手柄支持 </string>
|
||||
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄將會認作一個手柄 </string>
|
||||
<string name="title_checkbox_vibrate_fallback">以手機模擬手柄震動</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">當手柄不支援震動時, 以手機來模擬</string>
|
||||
<string name="title_seekbar_deadzone"> 調整手柄死區 </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>
|
||||
<string name="summary_checkbox_vibrate_fallback"> 如果你的手柄不支援震動,則震動設備以類比遊戲震動效果 </string>
|
||||
<string name="title_seekbar_deadzone"> 調整搖杆死區 </string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Xbox 360/One 手柄驅動 </string>
|
||||
<string name="summary_checkbox_xb1_driver"> 若要在那些沒有原生Xbox手柄驅動的設備上使用Xbox手柄,請勾上此複選框 </string>
|
||||
<string name="title_checkbox_usb_bind_all">覆蓋Android控制器支援</string>
|
||||
<string name="summary_checkbox_usb_bind_all">強制 Moonlight USB驅動程式接管所有支持的Xbox手柄</string>
|
||||
<string name="title_checkbox_mouse_emulation">透過手柄模擬滑鼠</string>
|
||||
<string name="summary_checkbox_mouse_emulation">長按 [Start] 鍵將會切換至滑鼠模式</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="summary_checkbox_xb1_driver"> 為缺少原生Xbox手柄支援的設備啟用內置USB驅動 </string>
|
||||
<string name="title_checkbox_usb_bind_all"> 覆蓋安卓手柄支援 </string>
|
||||
<string name="summary_checkbox_usb_bind_all"> 強制Moonlight的USB驅動接管所有受支持的Xbox手柄 </string>
|
||||
<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="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>
|
||||
<string name="summary_checkbox_show_onscreen_controls"> 將在串流畫面上顯示一層虛擬手柄 </string>
|
||||
<string name="title_checkbox_vibrate_osc">啟用震動</string>
|
||||
<string name="summary_checkbox_vibrate_osc">在手機上模擬搖桿震動</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="category_on_screen_controls_settings"> 螢幕控制按鈕設置 </string>
|
||||
<string name="title_checkbox_show_onscreen_controls"> 顯示幕幕控制按鈕 </string>
|
||||
<string name="summary_checkbox_show_onscreen_controls"> 在觸控式螢幕上顯示一層虛擬手柄 </string>
|
||||
<string name="title_checkbox_vibrate_osc"> 啟用震動 </string>
|
||||
<string name="summary_checkbox_vibrate_osc"> 使用螢幕控制按鈕時震動設備以類比遊戲震動效果 </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="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>
|
||||
<string name="category_ui_settings"> 介面設置 </string>
|
||||
<string name="title_language_list"> 語言 </string>
|
||||
<string name="summary_language_list"> 選擇Moonlight顯示的語言 </string>
|
||||
<string name="title_checkbox_small_icon_mode"> 使用小圖示 </string>
|
||||
<string name="summary_checkbox_small_icon_mode"> 使用小圖示以在螢幕上顯示更多專案 </string>
|
||||
|
||||
<string name="category_host_settings"> 主機設置 </string>
|
||||
<string name="title_checkbox_enable_sops"> 優化遊戲設置 </string>
|
||||
<string name="summary_checkbox_enable_sops"> 允許GeForce Experience為最佳串流效果自動更改遊戲設置 </string>
|
||||
<string name="title_checkbox_host_audio"> 將聲音輸出到電腦上 </string>
|
||||
<string name="summary_checkbox_host_audio"> 將在電腦和本設備同時輸出聲音 </string>
|
||||
<string name="summary_checkbox_enable_sops"> 允許GFE為最佳串流效果自動更改遊戲設置 </string>
|
||||
<string name="title_checkbox_host_audio"> 在電腦上播放聲音 </string>
|
||||
<string name="summary_checkbox_host_audio"> 在電腦和本設備同時輸出聲音 </string>
|
||||
|
||||
<string name="category_advanced_settings"> 高級設置 </string>
|
||||
<string name="title_disable_frame_drop">永遠不掉幀</string>
|
||||
<string name="summary_disable_frame_drop">在一些手機上可能可以減少micro-stuttering, 但可能導致延遲</string>
|
||||
<string name="title_video_format"> H.265設置 </string>
|
||||
<string name="summary_video_format">H.265能降低頻寬需求,但是需要設備支持 </string>
|
||||
<string name="title_enable_hdr">啟用 HDR (實驗中)</string>
|
||||
<string name="summary_enable_hdr">啟用 HDR 當遊戲與電腦的顯示卡支援時. HDR 需要 GTX 1000 系列之顯示卡或更高.</string>
|
||||
<string name="title_disable_frame_drop"> 永不掉幀 </string>
|
||||
<string name="summary_disable_frame_drop"> 可能會減少在一些設備上的卡頓,但會增加延遲 </string>
|
||||
<string name="title_video_format"> 更改H.265設置 </string>
|
||||
<string name="summary_video_format">H.265能降低視頻頻寬需求,但需要較新的設備才能支援</string>
|
||||
<string name="title_enable_hdr"> 啟用 HDR (實驗) </string>
|
||||
<string name="summary_enable_hdr"> 當遊戲和顯卡支援時以HDR模式串流。 HDR需要GTX 1000系列或更高規格顯卡。 </string>
|
||||
<string name="title_enable_perf_overlay"> 啟用性能資訊 </string>
|
||||
<string name="summary_enable_perf_overlay"> 在串流中顯示即時性能資訊 </string>
|
||||
<string name="title_enable_post_stream_toast">串流完畢顯示延遲資訊</string>
|
||||
<string name="summary_enable_post_stream_toast">串流結束後顯示延遲資訊</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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user