Compare commits

..

111 Commits

Author SHA1 Message Date
Cameron Gutman 7231f5468b Version 8.11 2020-03-25 00:07:53 -07:00
Cameron Gutman 4dfb0d7220 Fix crash during crash report generation 2020-03-22 13:48:17 -07:00
Cameron Gutman 2f4f53b048 Fix mouse back button closing the app with mouseNavButtons enabled 2020-03-21 15:34:03 -07:00
Cameron Gutman b6e8389544 Fix incorrect exception handling in JNI code 2020-03-21 14:30:31 -07:00
Cameron Gutman d113878613 Use current display refresh rate only for non-TV devices 2020-03-21 13:43:59 -07:00
Cameron Gutman f7ed7e06db Revert "Calculate FPS using the actual display refresh rate rather than the requested one"
This breaks refresh rate detection on the Shield Android TV.

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

* Create new default configuration

* Split Configuration Mode into separate Move and Resize modes

* Add transparency setting for on-screen buttons

* Updated translations for on-screen controls

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

This reverts commit 53dccbde2a.
2020-03-20 23:49:52 -07:00
Leo 9246ad412f Make it possible to press the RT button and the other buttons with the same finger 2020-03-13 18:48:50 +00:00
Cameron Gutman 1ccbbdd4fb Version 8.10 2020-03-08 19:48:53 -07:00
Cameron Gutman 16cf37994d Only suppress duplicate d-pad events if the hat has received input. Fixes #796 2020-03-04 18:48:14 -08:00
Cameron Gutman 01e84624c2 Remove stale moonlight-common reference from settings.gradle 2020-03-03 00:13:48 -08:00
Cameron Gutman 939cd7cf70 Update OkHttp to 3.12.10 2020-03-02 22:49:44 -08:00
Cameron Gutman 4b11603035 Fix back button on Shield Portable and standardize external/internal classification 2020-03-02 22:47:47 -08:00
Cameron Gutman ca18b6b052 Update to AGP 3.6.1 2020-03-01 13:12:04 -08:00
Cameron Gutman 3d0d19e561 Pass-through back button on external devices that don't look like gamepads 2020-03-01 12:45:00 -08:00
Cameron Gutman ae463a8735 Emulated button combos must not be pressed with other buttons 2020-02-26 20:38:53 -08:00
Cameron Gutman 7e797829ae Also destroy the mouse emulation timer on device disconnect 2020-02-26 20:29:28 -08:00
Cameron Gutman 431ed6bc5d Cancel the mouse emulation timer when the stream ends 2020-02-26 20:18:11 -08:00
Cameron Gutman e9bb711c42 Add Start+Back+LB+RB combo for disconnecting the session 2020-02-26 19:54:53 -08:00
Cameron Gutman 623bc5c156 Fix check for gamepad buttons. Fixes #788 2020-02-26 19:19:43 -08:00
Cameron Gutman cfefef4619 Downgrade OkHTTP to 3.12.8 due to square/okhttp#5826 2020-02-25 22:40:17 -08:00
Cameron Gutman 4a9a881c1f Add missing else block 2020-02-25 22:26:52 -08:00
Cameron Gutman 13a06d585c Update dependencies 2020-02-25 20:49:30 -08:00
Cameron Gutman 1c8ad64da0 Only set KEY_FRAME_RATE on M+ to reduce compatibility risk 2020-02-25 20:24:18 -08:00
Cameron Gutman 1d8925de57 Fix NDK version in Travis CI build 2020-02-25 00:38:41 -08:00
Cameron Gutman 0eb7e779b8 Update Travis CI to build-tools-29.0.3 2020-02-25 00:25:15 -08:00
Cameron Gutman a4b86eefe2 Change errorCode from long to int to fix 32-bit platforms 2020-02-24 23:24:22 -08:00
Cameron Gutman 902a58bc70 Improve video decoder init failure message 2020-02-24 23:23:23 -08:00
Cameron Gutman a34a44f29a Fix crash on Android 5.0 and earlier 2020-02-24 22:05:26 -08:00
Cameron Gutman 454fe80172 Update Gradle and AGP for AS 3.6.0 2020-02-24 21:49:14 -08:00
Cameron Gutman 81b6a8a311 Set the vendor.qti-ext-dec-low-latency.enable Qualcomm vendor extension 2020-02-22 17:06:32 -08:00
Cameron Gutman 3011a5bad7 Use the unmodified FPS value when sending the launch request 2020-02-22 01:28:41 -08:00
Cameron Gutman dcb7be3acd Use the original FPS value for KEY_FRAME_RATE 2020-02-22 01:18:11 -08:00
Cameron Gutman 68a6b510b1 Set KEY_FRAME_RATE for devices where KEY_OPERATING_RATE silently fails 2020-02-22 01:05:26 -08:00
Cameron Gutman dca3e89303 Log configured MediaFormat and achievable FPS ranges 2020-02-22 01:04:18 -08:00
Cameron Gutman bae6fef588 Log the actual input and output formats 2020-02-21 22:02:37 -08:00
Cameron Gutman 37f65e43a5 Add error code on connection failure dialog 2020-02-21 22:01:12 -08:00
Cameron Gutman 8c910101c7 Fix Lint errors on API level 16 2020-02-19 23:53:44 -08:00
Cameron Gutman 112d9c41eb Use KEY_LOW_LATENCY to request low-latency decoding on Android R 2020-02-19 23:40:06 -08:00
Cameron Gutman c91d1097f6 Set preferMinimalPostProcessing on Android R 2020-02-19 23:29:37 -08:00
Cameron Gutman 105c2c9eef Version 8.9 r2 2020-01-26 10:52:09 -08:00
Cameron Gutman b754d2de28 Fix crash with OSC disabled 2020-01-26 10:45:12 -08:00
Cameron Gutman f6425c7ec6 Version 8.9 2020-01-25 20:07:15 -08:00
Cameron Gutman e690c9b8c8 Fix build error due to Lollipop API 2020-01-19 15:46:47 -08:00
Cameron Gutman f87cbac77c Fix R3 button X position and move L3+R3 to the lower part of the screen 2020-01-18 23:14:51 -08:00
gotoAndDie 150bd313cf Increase usability of on-screen virtual controller (#782)
* Restore resize controls, Make buttons oval

* Create new default configuration

* Split Configuration Mode into separate Move and Resize modes
2020-01-18 23:13:07 -08:00
Cameron Gutman bc90cb894c Add German option to language picker and translate French option to French 2020-01-18 22:04:00 -08:00
Cameron Gutman c51a75a681 Merge branch 'translation-de' of https://github.com/uniqx/moonlight-android 2020-01-18 21:54:05 -08:00
bubuleur 68aa9bd12d Add French translation of arrays.xml 2020-01-18 21:51:21 -08:00
bubuleur 1fb6bf4d70 Update French (#768)
* Update French
2020-01-18 21:46:40 -08:00
Cameron Gutman b4df3658f1 Merge pull request #764 from ZerOri/master
Update strings.xml
2020-01-18 21:45:52 -08:00
Cameron Gutman 4efb4d9b24 Merge pull request #765 from ZerOri/patch-1
Update strings.xml
2020-01-18 21:45:29 -08:00
Cameron Gutman d61e893731 Centralize Discord invite links 2020-01-18 10:53:37 -08:00
Cameron Gutman 951e44728e Version 8.8.1 2020-01-03 22:05:58 -06:00
Cameron Gutman 8dcdf73222 Fix accidentally inverted condition for VUI parameter removal 2020-01-03 21:59:25 -06:00
Cameron Gutman 44c3b0af57 Version 8.8 2020-01-02 16:14:15 -06:00
Cameron Gutman 2b295400ac Avoid using RFI for HEVC on newer MediaTek SoCs 2020-01-02 16:13:19 -06:00
Cameron Gutman aa8d8e93d2 Whitelist newer Bravia devices for HEVC to minimize crashes 2019-12-31 12:59:20 -06:00
Cameron Gutman 89be7eac0e Update AGP to 3.5.3 2019-12-15 12:04:55 -08:00
Cameron Gutman f3847b932b Leave H.264 SPS VUI parameters in place on devices running API 26+ 2019-12-15 12:04:35 -08:00
Zero O 5e4f37532c Update strings.xml
Complete revision for cht translation according to the latest version
2019-12-05 19:40:58 +08:00
Zero O f3f5ca74a3 Update strings.xml
Complete revision for Chs translation
2019-12-05 19:31:12 +08:00
Cameron Gutman e50b7076a1 Version 8.7 2019-12-04 18:21:11 -08:00
Cameron Gutman 36ab5aa1b6 Update common-c to fix logic error in audio duration selection 2019-12-01 20:31:39 -08:00
Cameron Gutman a0a2b299d9 Merge pull request #758 from duchuule/hotfix1
fix bug where touch hitbox of analog stick is not full circle
2019-12-01 22:29:02 -06:00
Cameron Gutman 14d354fc29 Whitelist all C2 decoders for direct submit and HEVC 2019-12-01 20:20:57 -08:00
Cameron Gutman 342515f916 Force remote streaming optimizations if a VPN is active 2019-12-01 20:05:09 -08:00
Cameron Gutman 5f5944c237 Improve low bandwidth audio performance and fix RTSP issues with broken PMTUD 2019-11-30 22:14:32 -06:00
Cameron Gutman c025432ad6 Support 20 ms audio frames 2019-11-29 18:04:57 -06:00
Duc Le 171a6437fe fix bug where touch hitbox of analog stick is not full circle 2019-11-26 04:40:22 -06:00
Cameron Gutman 11b3648fac Fix auto-comment line breaks 2019-11-16 12:23:27 -08:00
Cameron Gutman d1fae89d6d Don't change level_idc for high refresh rate streams 2019-11-10 18:29:31 -08:00
Cameron Gutman 5c06848fe9 Version 8.6 2019-11-10 18:18:11 -08:00
Cameron Gutman b50e506e58 Attempt to fix line breaks in auto-comment response 2019-11-09 16:34:25 -08:00
Cameron Gutman 59fafa163d Add configuration for auto-comment bot 2019-11-09 15:00:13 -08:00
Cameron Gutman 22d84b5763 Bind to the underlying network when a VPN is connected 2019-11-09 12:57:54 -08:00
Cameron Gutman 6d186892a8 Fix errant touch events after a cancelled gesture 2019-11-09 11:23:50 -08:00
Cameron Gutman 88d6143897 Display a placeholder box art bitmap while loading box art 2019-11-05 00:19:58 -08:00
Cameron Gutman b729fba75e Update AGP to 3.5.2 2019-11-04 20:59:56 -08:00
Cameron Gutman c0d3f9fa48 Abort pairing if another pairing attempt is in progress 2019-11-04 20:27:05 -08:00
Cameron Gutman af5e7a0e33 Calculate FPS using the actual display refresh rate rather than the requested one 2019-11-04 20:22:12 -08:00
Cameron Gutman 371d96ea65 Fix VPN check on KitKat and below 2019-11-04 19:05:34 -08:00
Cameron Gutman e9e332ff85 Don't update the external IP address when connected to a VPN 2019-11-04 19:00:29 -08:00
Cameron Gutman e133ac2815 Version 8.5 2019-10-29 22:06:28 -07:00
Cameron Gutman 1dba5d147e Add a hack for massive video latency on Pixel 4 after display mode change 2019-10-29 21:38:06 -07:00
Cameron Gutman 1616c0b022 Fix codec capabilities on devices launching with Q and C2 codecs 2019-10-24 20:20:26 -07:00
Cameron Gutman bcee2cf0e3 Update moonlight-common-c submodule 2019-10-24 19:57:03 -07:00
Cameron Gutman 3e7ddab0e9 Blacklist 59 FPS on BRAVIA_ATV3 due to crash reports 2019-10-20 00:06:17 -07:00
Cameron Gutman 5da0177356 Convert tabs to spaces 2019-10-19 23:59:33 -07:00
Cameron Gutman 7e21638811 Don't double count USB attached Xbox One controllers 2019-10-16 19:26:24 -07:00
Cameron Gutman db5b7ab867 Version 8.4.1 2019-10-16 19:10:56 -07:00
Cameron Gutman 3bcc1c84bb Fix crash on controllers with RX and RY but no Z and RZ axes 2019-10-16 19:02:51 -07:00
Cameron Gutman d46053f8d6 Preserve old DS4 detection behavior on Android 4.3 and below 2019-10-15 21:15:03 -07:00
Cameron Gutman 00a5fed9e9 Update AGP to 3.5.1 2019-10-15 20:58:03 -07:00
Cameron Gutman b6315a715a Improve support for DualShock 4 and Xbox One controllers on 4.14+ kernels 2019-10-15 20:57:33 -07:00
Cameron Gutman 0da8303468 Don't use the USB driver for Xbox One gamepads on 4.14+ kernels 2019-10-15 20:05:01 -07:00
Cameron Gutman c821c4684f Allow FFmpeg decoders on Android x86. Closes #630 2019-10-15 00:11:43 -07:00
Cameron Gutman 6bae33f822 Merge pull request #739 from vanitasvitae/patch-1
Fix German short_description
2019-10-15 00:06:12 -07:00
Cameron Gutman 08d4ab67a6 Update moonlight-common-c submodule 2019-10-12 19:50:30 -07:00
Paul Schaub 62203d2f21 Fix German short_description
fixed a typo
2019-10-06 12:44:40 +02:00
Cameron Gutman 4968dcc558 Version 8.3 2019-09-14 20:23:46 -07:00
Cameron Gutman 6d66d1371f Fix TV view padding on Android Q 2019-09-14 20:14:31 -07:00
Cameron Gutman b87ca71103 Treat all InputDevices as external on the Tinker Board 2019-09-14 20:08:26 -07:00
Cameron Gutman c251cd2e8f Fix control stream connection error on multi-homed hosts 2019-09-14 14:11:14 -07:00
Cameron Gutman 593616d2d9 Fix layout transitions on foldable devices 2019-09-08 11:11:02 -07:00
Michael Pöhn fdd4c0bbe1 german translation 2019-07-17 14:15:58 +02:00
86 changed files with 4637 additions and 3578 deletions
+4
View 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!
+5 -2
View File
@@ -8,8 +8,11 @@ android:
components:
- tools
- platform-tools
- build-tools-29.0.1
- build-tools-29.0.3
- android-29
before_install:
- sdkmanager --list
install:
- yes | sdkmanager "ndk-bundle"
- yes | sdkmanager "ndk;20.0.5594570"
+6 -6
View File
@@ -7,8 +7,8 @@ android {
minSdkVersion 16
targetSdkVersion 29
versionName "8.2"
versionCode = 199
versionName "8.11"
versionCode = 216
}
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'
}
+29 -2
View File
@@ -26,6 +26,7 @@ import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
@@ -96,8 +97,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
PreferenceConfiguration.readPreferences(AppView.this),
computer, localBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
@@ -147,6 +147,27 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
};
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// If appGridAdapter is initialized, let it know about the configuration change.
// If not, it will pick it up when it initializes.
if (appGridAdapter != null) {
// Update the app grid adapter to create grid items with the correct layout
appGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
try {
// Reinflate the app grid itself to pick up the layout change
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
private void startComputerUpdates() {
// Don't start polling if we're not bound or in the foreground
if (managerBinder == null || !inForeground) {
@@ -496,6 +517,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;
}
}
+122 -35
View File
@@ -27,6 +27,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 +43,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;
@@ -72,6 +74,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;
@@ -397,37 +400,55 @@ 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)
@@ -575,6 +596,29 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
// FIXME: Remove when Android R SDK is finalized
private static void setPreferMinimalPostProcessingWithReflection(WindowManager.LayoutParams windowLayoutParams, boolean isPreferred) {
// Build.VERSION.PREVIEW_SDK_INT was added in M
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && Build.VERSION.PREVIEW_SDK_INT == 0) {
// Don't attempt this reflection unless on Android R Developer Preview
return;
}
}
else {
return;
}
try {
Field field = windowLayoutParams.getClass().getDeclaredField("preferMinimalPostProcessing");
field.set(windowLayoutParams, isPreferred);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private float prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
@@ -651,6 +695,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
displayRefreshRate = display.getRefreshRate();
}
// Enable HDMI ALLM (game mode) on Android R
setPreferMinimalPostProcessingWithReflection(windowLayoutParams, true);
// Apply the display mode change
getWindow().setAttributes(windowLayoutParams);
@@ -685,7 +732,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")
@@ -944,11 +1002,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 +1038,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 +1073,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;
}
@@ -1088,7 +1167,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// 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;
}
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
@@ -1179,8 +1260,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
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;
}
@@ -1257,6 +1339,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
break;
case MotionEvent.ACTION_CANCEL:
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
}
break;
default:
return false;
}
@@ -1352,7 +1439,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 +1454,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() {
+16 -16
View File
@@ -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));
}
}
+4 -3
View File
@@ -126,6 +126,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Set default preferences if we've never been run
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Set the correct layout for the PC grid
pcGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
// Setup the list view
ImageButton settingsButton = findViewById(R.id.settingsButton);
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
@@ -223,9 +226,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this,
PreferenceConfiguration.readPreferences(this).listMode,
PreferenceConfiguration.readPreferences(this).smallIconMode);
pcGridAdapter = new PcGridAdapter(this, PreferenceConfiguration.readPreferences(this));
initializeViews();
}
@@ -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,7 +64,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
}
@Override
public int setup(int audioConfiguration) {
public int setup(int audioConfiguration, int sampleRate, int samplesPerFrame) {
int channelConfig;
int bytesPerFrame;
@@ -72,11 +72,11 @@ public class AndroidAudioRenderer implements AudioRenderer {
{
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
bytesPerFrame = 2 * 240 * 2;
bytesPerFrame = 2 * samplesPerFrame * 2;
break;
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
bytesPerFrame = 6 * 240 * 2;
bytesPerFrame = 6 * samplesPerFrame * 2;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
@@ -122,7 +122,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 +135,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 +170,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");
}
}
@@ -1,5 +1,6 @@
package com.limelight.binding.input;
import android.app.Activity;
import android.content.Context;
import android.hardware.input.InputManager;
import android.hardware.usb.UsbDevice;
@@ -51,7 +52,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
private final NvConnection conn;
private final Context activityContext;
private final Activity activityContext;
private final double stickDeadzone;
private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
@@ -61,7 +62,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final PreferenceConfiguration prefConfig;
private short currentControllers, initialControllers;
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
public ControllerHandler(Activity activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
this.activityContext = activityContext;
this.conn = conn;
this.gestures = gestures;
@@ -108,6 +109,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
defaultContext.assignedControllerNumber = true;
defaultContext.external = false;
// Some devices (GPD XD) have a back button which sends input events
// with device ID == 0. This hits the default context which would normally
@@ -146,6 +148,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (context != null) {
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
releaseControllerNumber(context);
context.destroy();
inputDeviceContexts.remove(deviceId);
}
}
@@ -160,10 +163,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public void stop() {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
deviceContext.destroy();
}
if (deviceContext.vibrator != null) {
deviceContext.vibrator.cancel();
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
deviceContext.destroy();
}
deviceVibrator.cancel();
@@ -215,9 +220,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 +269,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
InputDeviceContext devContext = (InputDeviceContext) context;
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
if (devContext.name != null &&
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
if (!devContext.external) {
LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0;
}
@@ -326,6 +330,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.id = device.getControllerId();
context.device = device;
context.external = true;
context.leftStickDeadzoneRadius = (float) stickDeadzone;
context.rightStickDeadzoneRadius = (float) stickDeadzone;
@@ -335,6 +340,24 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
private static boolean isExternal(InputDevice dev) {
// The ASUS Tinker Board inaccurately reports Bluetooth gamepads as internal,
// causing shouldIgnoreBack() to believe it should pass through back as a
// navigation event for any attached gamepads.
if (Build.MODEL.equals("Tinker Board")) {
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();
@@ -368,9 +391,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;
}
@@ -393,11 +414,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;
}
}
@@ -409,8 +433,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) {
@@ -418,10 +445,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
String devName = dev.getName();
LimeLog.info("Creating controller context for device: "+devName);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
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 (dev.getVibrator().hasVibrator()) {
context.vibrator = dev.getVibrator();
@@ -464,25 +496,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 (android.os.Build.VERSION.SDK_INT >= android.os.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;
}
}
@@ -588,7 +640,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;
}
}
}
@@ -767,7 +819,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
if (context.isDualShock4) {
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;
@@ -812,7 +878,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;
@@ -842,18 +908,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
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;
}
}
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
@@ -984,17 +1039,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;
}
}
@@ -1209,15 +1268,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:
@@ -1301,6 +1376,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
sendControllerInputPacket(context);
if (context.pendingExit && context.inputMap == 0) {
// All buttons from the quit combo are lifted. Finish the activity now.
activityContext.finish();
}
return true;
}
@@ -1331,15 +1412,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:
@@ -1385,9 +1482,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return false;
}
// Start+Back+LB+RB is the quit combo
if (context.inputMap == (ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG |
ControllerPacket.LB_FLAG | ControllerPacket.RB_FLAG)) {
// Wait for the combo to lift and then finish the activity
context.pendingExit = true;
}
// Start+LB acts like select for controllers with one button
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
@@ -1397,10 +1501,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
// We detect select+start or start+RB as the special button combo
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG) ||
context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
@@ -1408,7 +1512,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
// We don't need to send repeat key down events, but the platform
// sends us events that claim to be repeats but they're from different
// devices, so we just send them all and deal with some duplicates.
@@ -1479,6 +1582,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());
}
}
@@ -1491,6 +1595,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
class GenericControllerContext {
public int id;
public boolean external;
public float leftStickDeadzoneRadius;
public float rightStickDeadzoneRadius;
@@ -1511,6 +1616,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 {
@@ -1530,15 +1642,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 boolean pendingExit;
public int emulatingButtonFlags = 0;
@@ -1551,9 +1665,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);
}
}
@@ -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));
}
@@ -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);
@@ -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,182 @@ 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 = 4;
private static final int ANALOG_L_BASE_Y = 1;
private static final int ANALOG_R_BASE_X = 96;
private static final int ANALOG_R_BASE_Y = 42;
private static final int ANALOG_SIZE = 28;
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)
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)
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)
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)
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 +344,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 +362,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,7 +318,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (lowLatency) {
videoFormat.setInteger(MediaCodecHelper.KEY_LOW_LATENCY, 1);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Set the Qualcomm vendor low latency extension if the Android R option is unavailable
if (MediaCodecHelper.decoderSupportsQcomVendorLowLatency(selectedDecoderName)) {
// MediaCodec supports vendor-defined format keys using the "vendor.<extension name>.<parameter name>" syntax.
// These allow access to functionality that is not exposed through documented MediaFormat.KEY_* values.
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/common/inc/vidc_vendor_extensions.h;l=67
//
// Examples of Qualcomm's vendor extensions for Snapdragon 845:
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/vdec/src/omx_vdec_extensions.hpp
// https://cs.android.com/android/_/android/platform/hardware/qcom/sm8150/media/+/0621ceb1c1b19564999db8293574a0e12952ff6c
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
}
// Operate at maximum rate to lower latency as much as possible on
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
// but that will actually result in the decoder crashing if it can't satisfy
@@ -308,8 +341,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
}
configuredFormat = videoFormat;
LimeLog.info("Configuring with format: "+configuredFormat);
try {
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// This will contain the actual accepted input format attributes
inputFormat = videoDecoder.getInputFormat();
LimeLog.info("Input format: "+inputFormat);
}
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -334,6 +377,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
legacyInputBuffers = videoDecoder.getInputBuffers();
}
} catch (Exception e) {
e.printStackTrace();
return -5;
@@ -445,7 +489,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
outputFormat = videoDecoder.getOutputFormat();
LimeLog.info("New output format: " + outputFormat);
break;
default:
break;
@@ -683,17 +728,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 +763,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 +1063,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,7 @@
package com.limelight.grid;
import android.app.Activity;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -13,6 +14,7 @@ import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.Collections;
import java.util.Comparator;
@@ -23,15 +25,37 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
private final CachedAppAssetLoader loader;
private final ComputerDetails computer;
private final String uniqueId;
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
private CachedAppAssetLoader loader;
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId) {
super(context, getLayoutIdForPreferences(prefs));
this.computer = computer;
this.uniqueId = uniqueId;
updateLayoutWithPreferences(context, prefs);
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
}
else if (prefs.smallIconMode) {
return R.layout.app_grid_item_small;
}
else {
return R.layout.app_grid_item;
}
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
int dpi = context.getResources().getDisplayMetrics().densityDpi;
int dp;
if (small) {
if (prefs.smallIconMode) {
dp = SMALL_WIDTH_DP;
}
else {
@@ -45,10 +69,19 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
if (loader != null) {
// Cancel operations on the old loader
cancelQueuedOperations();
}
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));
}
public void cancelQueuedOperations() {
@@ -10,22 +10,32 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected final Context context;
protected final int layoutId;
protected final ArrayList<T> itemList = new ArrayList<>();
protected final LayoutInflater inflater;
private int layoutId;
final ArrayList<T> itemList = new ArrayList<>();
private final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId) {
GenericGridAdapter(Context context, int layoutId) {
this.context = context;
this.layoutId = layoutId;
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
void setLayoutId(int layoutId) {
if (layoutId != this.layoutId) {
this.layoutId = layoutId;
// Force the view to be redrawn with the new layout
notifyDataSetInvalidated();
}
}
public void clear() {
itemList.clear();
}
@@ -10,14 +10,32 @@ import com.limelight.PcView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
public PcGridAdapter(Context context, PreferenceConfiguration prefs) {
super(context, getLayoutIdForPreferences(prefs));
}
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;
}
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
// This will trigger the view to reload with the new layout
setLayoutId(getLayoutIdForPreferences(prefs));
}
public void addComputer(PcView.ComputerObject computer) {
@@ -52,15 +52,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);
}
@@ -188,9 +190,10 @@ public class CachedAppAssetLoader {
prgView.setVisibility(View.VISIBLE);
}
// Set off another loader task on the network executor
// Set off another loader task on the network executor. This time our AsyncDrawable
// will use the app image placeholder bitmap, rather than an empty bitmap.
LoaderTask task = new LoaderTask(imageView, prgView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
imageView.setVisibility(View.VISIBLE);
imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple);
@@ -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,318 @@ 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;
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();
}
public void stop() {
// Interrupt any pending connection. This is thread-safe.
MoonBridge.interruptConnection();
public void stop() {
// Interrupt any pending connection. This is thread-safe.
MoonBridge.interruptConnection();
// 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();
}
// 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();
}
// 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);
// 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);
String serverInfo = h.getServerInfo();
context.serverAppVersion = h.getServerVersion(serverInfo);
if (context.serverAppVersion == null) {
context.connListener.displayMessage("Server version malformed");
return false;
}
String serverInfo = h.getServerInfo();
context.serverAppVersion = h.getServerVersion(serverInfo);
if (context.serverAppVersion == null) {
context.connListener.displayMessage("Server version malformed");
return false;
}
// 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;
}
// 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;
}
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);
}
}
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);
}
}
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;
}
}
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;
}
}
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;
}
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;
}
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();
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();
String appName = context.streamConfig.getApp().getAppName();
String appName = context.streamConfig.getApp().getAppName();
context.serverAddress = host;
context.connListener.stageStarting(appName);
context.serverAddress = host;
context.connListener.stageStarting(appName);
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;
}
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;
}
ByteBuffer ib = ByteBuffer.allocate(16);
ib.putInt(context.riKeyId);
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;
}
// 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.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);
}
}
// 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(),
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);
}
}
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
}
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,241 @@ 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 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;
private static final int CHANNEL_COUNT_STEREO = 2;
private static final int CHANNEL_COUNT_5_1 = 6;
private static final int CHANNEL_MASK_STEREO = 0x3;
private static final int CHANNEL_MASK_5_1 = 0xFC;
private NvApp app;
private int width, height;
private int refreshRate;
private int launchRefreshRate;
private int clientRefreshRateX100;
private int bitrate;
private boolean sops;
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 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 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 setHevcBitratePercentageMultiplier(int multiplier) {
config.hevcBitratePercentageMultiplier = multiplier;
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 setEnableHdr(boolean enableHdr) {
config.enableHdr = enableHdr;
return this;
}
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
config.hevcBitratePercentageMultiplier = multiplier;
return this;
}
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
config.attachedGamepadMask = attachedGamepadMask;
return this;
}
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
config.enableHdr = enableHdr;
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 setAttachedGamepadMask(int attachedGamepadMask) {
config.attachedGamepadMask = attachedGamepadMask;
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 setAttachedGamepadMaskByCount(int gamepadCount) {
config.attachedGamepadMask = 0;
for (int i = 0; i < 4; i++) {
if (gamepadCount > i) {
config.attachedGamepadMask |= 1 << i;
}
}
return this;
}
config.audioConfiguration = audioConfig;
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");
}
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;
}
config.audioConfiguration = audioConfig;
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;
}
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.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 getAudioConfiguration() {
return audioConfiguration;
}
public boolean getHevcSupported() {
return supportsHevc;
}
public int getLaunchRefreshRate() {
return launchRefreshRate;
}
public int getBitrate() {
return bitrate;
}
public int getMaxPacketSize() {
return maxPacketSize;
}
public int getHevcBitratePercentageMultiplier() {
return hevcBitratePercentageMultiplier;
}
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 boolean getEnableHdr() {
return enableHdr;
}
public int getAudioConfiguration() {
return audioConfiguration;
}
public boolean getHevcSupported() {
return supportsHevc;
}
public int getAttachedGamepadMask() {
return attachedGamepadMask;
}
public int getHevcBitratePercentageMultiplier() {
return hevcBitratePercentageMultiplier;
}
public int getClientRefreshRateX100() {
return clientRefreshRateX100;
}
public boolean getEnableHdr() {
return enableHdr;
}
public int getAttachedGamepadMask() {
return attachedGamepadMask;
}
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,13 @@
package com.limelight.nvstream.av.audio;
public interface AudioRenderer {
int setup(int audioConfiguration);
int setup(int 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("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();
}
}
@@ -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,59 @@ 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;
}
}
File diff suppressed because it is too large Load Diff
@@ -18,323 +18,324 @@ 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();
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);
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];
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];
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);
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);
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.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));
}
public X509Certificate getPairedCert() {
return serverCert;
}
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
PairingHashAlgorithm hashAlgo;
public X509Certificate getPairedCert() {
return serverCert;
}
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
PairingHashAlgorithm hashAlgo;
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);
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);
// 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;
}
// 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;
}
// 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;
}
// 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;
}
// 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);
// 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);
// 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;
}
// 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) {
// 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;
}
}
}
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;
}
}
}
}
@@ -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;
}
@@ -84,9 +84,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(audioConfiguration, sampleRate, samplesPerFrame);
}
else {
return -1;
@@ -129,7 +129,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 +141,7 @@ public class MoonBridge {
}
}
public static void bridgeClConnectionTerminated(long errorCode) {
public static void bridgeClConnectionTerminated(int errorCode) {
if (connectionListener != null) {
connectionListener.connectionTerminated(errorCode);
}
@@ -208,7 +208,7 @@ public class MoonBridge {
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;
}
}
@@ -19,6 +19,7 @@ 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";
@@ -46,6 +47,7 @@ public class PreferenceConfiguration {
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;
@@ -73,6 +75,7 @@ public class PreferenceConfiguration {
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;
@@ -317,6 +320,8 @@ public class PreferenceConfiguration {
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
@@ -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);
@@ -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));
}
}
}
}
@@ -68,17 +68,6 @@ public class UiHelper {
View rootView = activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
{
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Allow this non-streaming activity to layout under notches.
//
@@ -89,7 +78,16 @@ public class UiHelper {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Draw under the status bar on Android Q devices
// Using getDecorView() here breaks the translucent status/navigation bar when gestures are disabled
@@ -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;
}
}
+36 -74
View File
@@ -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);
@@ -178,6 +159,8 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
decodeUnit->frameNumber, decodeUnit->receiveTimeMs);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
return DR_OK;
}
else if (ret != DR_OK) {
@@ -192,22 +175,27 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
currentEntry = currentEntry->next;
}
return (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
decodeUnit->frameNumber,
decodeUnit->receiveTimeMs);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
return DR_OK;
}
else {
return ret;
}
}
int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags) {
JNIEnv* env = GetThreadEnv();
int err;
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,20 +237,12 @@ 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);
int decodeLen = opus_multistream_decode(Decoder,
@@ -284,6 +256,10 @@ void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
(*env)->ReleaseShortArrayElements(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
@@ -294,71 +270,56 @@ void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
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();
if ((*env)->ExceptionCheck(env)) {
return;
}
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClRumbleMethod, controllerNumber, lowFreqMotor, highFreqMotor);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
}
}
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 +343,7 @@ static AUDIO_RENDERER_CALLBACKS BridgeAudioRendererCallbacks = {
.stop = BridgeArStop,
.cleanup = BridgeArCleanup,
.decodeAndPlaySample = BridgeArDecodeAndPlaySample,
.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION
};
static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
+2 -2
View File
@@ -83,8 +83,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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+8 -8
View File
@@ -8,14 +8,6 @@
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"
@@ -24,6 +16,14 @@
android:layout_width="150dp"
android:layout_height="175dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="75dp"
android:layout_height="75dp"
android:indeterminate="true">
</ProgressBar>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
@@ -8,14 +8,6 @@
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"
@@ -24,6 +16,14 @@
android:layout_width="100dp"
android:layout_height="117dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminate="true">
</ProgressBar>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
+14
View 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>
+192
View File
@@ -0,0 +1,192 @@
<?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="title_checkbox_51_surround">5.1 Surround Sound aktivieren</string>
<string name="summary_checkbox_51_surround">Deaktivieren wenn die Audiowiedergabe fehlerhaft ist. Setzt GFE 2.7 oder neuer voraus.</string>
<string name="category_input_settings">Eingabe Einstellungen</string>
<string name="title_checkbox_multi_controller">Automatische GamePad-Erkennung</string>
<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_list_mode">Zeige Listen anstelle von Rastern.</string>
<string name="summary_checkbox_list_mode">Zeige Apps und PCs als Liste anstelle eines Rasters</string>
<string name="title_checkbox_small_icon_mode">Verwende kleine Icons</string>
<string name="summary_checkbox_small_icon_mode">Verwende kleine Icons in der Rasteransicht, um mehr gleichzeitig anzeigen zu können</string>
<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>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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>
+7 -6
View File
@@ -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>
@@ -138,8 +138,8 @@
<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>
@@ -187,5 +187,6 @@
<string name="summary_enable_hdr">Diffuser du HDR lorsque le jeu et le processeur graphique du PC le prennent en charge. HDR nécessite un GPU série GTX 1000 ou une version ultérieure.</string>
<string name="title_enable_perf_overlay">Activer la superposition de performance</string>
<string name="summary_enable_perf_overlay">Afficher une superposition à l\'écran avec des informations de performance en temps réel pendant la lecture en continu</string>
<string name="suffix_osc_opacity">%</string>
</resources>
+2 -1
View File
@@ -159,5 +159,6 @@
<string name="summary_video_format">H.265 riduce i requisiti di larghezza di banda video ma richiede un dispositivo molto recente</string>
<string name="title_enable_hdr">Abilita HDR (sperimentale)</string>
<string name="summary_enable_hdr">Utilizza l\'HDR quando il gioco e la scheda video del PC lo supportano. L\'HDR richiede una scheda video serie GTX 1000 o sucessive.</string>
<string name="suffix_osc_opacity">%</string>
</resources>
+13
View File
@@ -121,5 +121,18 @@
<string name="category_advanced_settings">高度な設定</string>
<string name="title_video_format">H.265</string>
<string name="summary_video_format">H.265は動画に必要な帯域幅を圧縮します。この機能にはなるべく新しいデバイスが必要です</string>
<string name="suffix_osc_opacity">%</string>
<string name="dialog_title_osc_opacity">透過率</string>
<string name="title_osc_opacity">透過率</string>
<string name="summary_osc_opacity">オンスクリーンコントローラの透過率を調整します</string>
<string name="title_only_l3r3">L3 と R3 のみ表示します</string>
<string name="summary_only_l3r3">L3 と R3 以外のボタンを表示しない</string>
<string name="title_reset_osc">オンスクリーンコントローラをデフォルトに戻します</string>
<string name="summary_reset_osc">サイズやレイアウトを戻します</string>
<string name="dialog_title_reset_osc">デフォルトに戻します</string>
<string name="dialog_text_reset_osc">本当にデフォルトに戻しますか?</string>
<string name="toast_reset_osc_success">オンスクリーンコントローラをデフォルトに戻しました</string>
<string name="title_checkbox_vibrate_osc">振動</string>
<string name="summary_checkbox_vibrate_osc">コントローラの振動を真似します</string>
</resources>
+1
View File
@@ -136,5 +136,6 @@
<string name="category_advanced_settings">고급 설정</string>
<string name="title_video_format">H.265 설정 변경</string>
<string name="summary_video_format">H.265는 비디오 대역폭 요구사항을 낮춰주지만 최신 장치가 필요합니다.</string>
<string name="suffix_osc_opacity">%</string>
</resources>
+1
View File
@@ -125,5 +125,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>
+1
View File
@@ -183,4 +183,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>
+122 -71
View File
@@ -1,12 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight</string>
<string name="app_label_root" translatable="false">Moonlight Root</string>
<!-- 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 +20,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 +49,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 +104,111 @@
<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="summary_checkbox_51_surround"> 如果你的声音听起来有问题请禁用。 \n 需要GFE 2.7或更高版本 </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_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="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_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="summary_checkbox_list_mode"> 列表显示电脑和游戏 </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_osc_opacity">更改屏幕按钮透明度</string>
<string name="dialog_title_osc_opacity">透明度</string>
<string name="suffix_osc_opacity">%</string>
<string name="summary_osc_opacity">令屏幕按钮变得更透明/更不透明</string>
</resources>
+134 -87
View File
@@ -1,38 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight</string>
<string name="app_label_root" translatable="false">Moonlight Root</string>
<!-- 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 +49,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 +104,111 @@
<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="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="summary_checkbox_51_surround"> 如果你的聲音聽起來有問題請禁用。 \n 需要GFE 2.7或更高版本 </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_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"> 強制MoonlightUSB驅動接管所有支持的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="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"> 只顯示L3R3 </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_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_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_osc_opacity">更改屏幕按鈕透明度</string>
<string name="dialog_title_osc_opacity">透明度</string>
<string name="suffix_osc_opacity">%</string>
<string name="summary_osc_opacity">令屏幕按钮變得更透明/更不透明</string>
</resources>
+24 -22
View File
@@ -31,30 +31,32 @@
</string-array>
<string-array name="language_names" translatable="false">
<item>Default</item>
<item>English</item>
<item>Italiano</item>
<item>日本語</item>
<item>Русский</item>
<item>Nederlands</item>
<item>中文(简体)</item>
<item>中文(繁體)</item>
<item>Korean</item>
<item>Español</item>
<item>French</item>
<item>Default</item>
<item>English</item>
<item>Italiano</item>
<item>日本語</item>
<item>Русский</item>
<item>Nederlands</item>
<item>中文(简体)</item>
<item>中文(繁體)</item>
<item>Korean</item>
<item>Español</item>
<item>Français</item>
<item>Deutsch</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>it</item>
<item>ja</item>
<item>ru</item>
<item>nl</item>
<item>zh-CN</item>
<item>zh-TW</item>
<item>ko</item>
<item>es</item>
<item>fr</item>
<item>default</item>
<item>en</item>
<item>it</item>
<item>ja</item>
<item>ru</item>
<item>nl</item>
<item>zh-CN</item>
<item>zh-TW</item>
<item>ko</item>
<item>es</item>
<item>fr</item>
<item>de</item>
</string-array>
<string-array name="decoder_names">
+5
View File
@@ -60,6 +60,7 @@
<string name="message_decoding_reset">Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default.</string>
<string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
<string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string>
<string name="video_decoder_init_failed">Video decoder failed to initialize. Your device may not support the selected resolution or frame rate.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
@@ -165,6 +166,10 @@
<string name="dialog_title_reset_osc">Reset Layout</string>
<string name="dialog_text_reset_osc">Are you sure you want to delete your saved on-screen controls layout?</string>
<string name="toast_reset_osc_success">On-screen controls reset to default</string>
<string name="title_osc_opacity">Change opacity of on-screen controls</string>
<string name="summary_osc_opacity">Make the on-screen controls more/less transparent</string>
<string name="dialog_title_osc_opacity">Change opacity</string>
<string name="suffix_osc_opacity">%</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
+11
View File
@@ -107,6 +107,17 @@
android:key="checkbox_only_show_L3R3"
android:summary="@string/summary_only_l3r3"
android:title="@string/title_only_l3r3" />
<com.limelight.preferences.SeekBarPreference
android:key="seekbar_osc_opacity"
android:dependency="checkbox_show_onscreen_controls"
android:dialogMessage="@string/summary_osc_opacity"
seekbar:min="0"
seekbar:step="1"
android:max="100"
android:defaultValue="90"
android:summary="@string/summary_osc_opacity"
android:text="@string/suffix_osc_opacity"
android:title="@string/dialog_title_osc_opacity" />
<com.limelight.preferences.ConfirmDeleteOscPreference
android:title="@string/title_reset_osc"
android:summary="@string/summary_reset_osc"
@@ -1,5 +1,5 @@
package com.limelight;
public class LimelightBuildProps {
public static final boolean ROOT_BUILD = false;
public static final boolean ROOT_BUILD = false;
}
@@ -1,5 +1,5 @@
package com.limelight;
public class LimelightBuildProps {
public static final boolean ROOT_BUILD = true;
public static final boolean ROOT_BUILD = true;
}
+1 -1
View File
@@ -5,7 +5,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:3.6.1'
}
}
@@ -1 +1 @@
Spiele vom deinem PC auf Android spielen (nur NVIDIA)
Spiele von deinem PC auf Android spielen (nur NVIDIA)
@@ -0,0 +1,4 @@
- Fixed various UI bugs on foldable Android devices
- Fixed connecting to a PC with multiple network connections
- Fixed overscan padding on Android TV 10
- Fixed gamepad back buttons not working on the ASUS Tinker Board
@@ -0,0 +1,4 @@
- Fixed DualShock 4 mapping on devices running 4.14+ kernels
- Improved support for wired Xbox 360/One controllers
- Fixed crash using certain controllers without analog triggers
- Enabled streaming on Android-x86
@@ -0,0 +1,4 @@
- Optimized for new devices launching with Android 10
- Added a workaround to avoid video lag on the Pixel 4
- Fixed duplicate gamepads being created when using a USB Xbox One gamepad
- Fixed crashes on Sony Bravia Android TV devices
@@ -0,0 +1,3 @@
- Fixed false touch events when using the back gesture on Android 10
- Fixed external IP address detection with certain VPN apps
- Display a placeholder image when box art is loading
@@ -0,0 +1,4 @@
- Fixed RTSP handshake error when streaming from certain networks
- Improved performance when streaming over a VPN
- Reduced audio bandwidth usage when streaming over low speed connections
- Fixed hitbox of on-screen analog sticks
@@ -0,0 +1,3 @@
- Pass-through H.264 colorspace data on Android 8.0 and later
- Fix hangs using HEVC on some MediaTek-based devices
- Default to HEVC on Sony Bravia TVs running Android 8.0 or later
@@ -0,0 +1 @@
- Fixed H.264 colorspace data on Android 8.0 and later
@@ -0,0 +1,4 @@
- Improved layout and appearance of the on-screen controls
- Added support for resizing the on-screen controls
- Added German translation
- Updated Chinese and French translations
@@ -0,0 +1,6 @@
- Latency optimizations for Android R DP1
- Video decoder optimizations
- Added Start+Back+LB+RB combo to disconnect the stream
- Fixed back button on some Android TV remotes
- Fixed d-pad on gamepads that expose non-functional hat axes
- Fixed unexpected mouse input after using gamepad mouse emulation mode
@@ -0,0 +1,4 @@
- Configurable transparency for on-screen controls
- Fixed a crash when pinning an app shortcut to the home screen
- Fixed right click unexpectedly stopping the stream on some devices
- Improved key repeating behavior while streaming
+2 -2
View File
@@ -1,6 +1,6 @@
#Tue Aug 20 11:37:45 PDT 2019
#Mon Feb 24 12:32:24 PST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
+1 -1
View File
@@ -1 +1 @@
include ':app', ':moonlight-common'
include ':app'