Compare commits

...

106 Commits

Author SHA1 Message Date
Cameron Gutman ad3614c58e Version 8.1 2019-08-07 23:39:28 -07:00
Cameron Gutman 9401ecc9fb Fix location of 197 changelog 2019-08-07 23:21:17 -07:00
Cameron Gutman 1711e5e1a4 Update common to fix termination detection and STUN fallback 2019-08-07 23:19:01 -07:00
Cameron Gutman 8eb4014f01 Fix build 2019-08-07 23:02:28 -07:00
Cameron Gutman df0d7952db Merge pull request #727 from bubuleur/patch-5
Update french language
2019-08-07 22:59:54 -07:00
Cameron Gutman 77d1770063 Tweak padding and spacing 2019-08-07 22:58:29 -07:00
Cameron Gutman f433bfdc02 Use an edge-to-edge layout for Android Q 2019-08-07 22:01:46 -07:00
Cameron Gutman f75b6f9b80 Remove redundant LinearLayout 2019-08-07 21:09:56 -07:00
Cameron Gutman 621df9996d Remove extra view padding for TV 2019-08-07 20:27:11 -07:00
Cameron Gutman 6c29503db9 Move the Android TV banner into the correct drawable folder. Fixes #728 2019-08-07 20:14:27 -07:00
Cameron Gutman 304a02e2ec Add Travis CI badge 2019-08-07 01:37:02 -07:00
Cameron Gutman 7aea7ed8c6 Add Travis CI support 2019-08-07 01:22:51 -07:00
Cameron Gutman e5ab3baa7b Fix Lint error in BouncyCastle due to javax references 2019-08-07 01:11:17 -07:00
bubuleur 41b73f7cd9 Update french language 2019-08-01 11:22:04 +02:00
Cameron Gutman 38da42caf3 Ignore .cxx folder 2019-07-28 11:40:46 -07:00
Cameron Gutman 424d71fa13 Update common to fix IPv6 WoL and GFE 3.19 graceful termination 2019-07-28 11:39:16 -07:00
Cameron Gutman dbc9d78002 Fix PiP overlay hiding with OSC disabled 2019-07-28 11:38:35 -07:00
Cameron Gutman b7ef8f54b7 Allow installation on external storage 2019-07-28 11:38:35 -07:00
Cameron Gutman bea7cab0c3 Hide overlays in PiP mode 2019-07-28 11:38:35 -07:00
Cameron Gutman 352b6f7dd9 Delete cached box art when deleting a PC 2019-07-28 11:38:35 -07:00
Cameron Gutman 8665fe364f Merge pull request #725 from GinVavilon/restore-program-after-user-remove
Delete program if it is removed by user
2019-07-28 11:38:11 -07:00
Cameron Gutman 7d023c8865 Merge pull request #726 from GinVavilon/update-ru-strings
Update Russian translation
2019-07-28 11:37:14 -07:00
GinVavilon 503d4b970c Update Russian translation 2019-07-28 21:25:16 +03:00
GinVavilon 6b07072a08 Delete program if it is removed by user
Fix problem: if user removes program game is not shown on launch
2019-07-28 20:36:17 +03:00
Cameron Gutman c873bae3e4 Merge pull request #723 from Poussinou/patch-1
Update README.md
2019-07-23 16:41:13 -04:00
Poussinou 7397a97a9e Update README.md 2019-07-23 11:27:40 +02:00
Cameron Gutman b567db9ab7 Version 8.0 2019-07-19 20:34:34 -07:00
Cameron Gutman 3440f54598 Add changelog for v8.0 2019-07-19 19:35:08 -07:00
Cameron Gutman d533b25b29 Fix typo in v7.4 changelog name 2019-07-19 19:33:49 -07:00
Cameron Gutman 72290bd725 Update full description to use F-droid compatible formatting 2019-07-19 19:31:07 -07:00
Cameron Gutman 0ac83e1cf7 Add HDR state to app data in shortcut trampoline 2019-07-16 22:57:30 -07:00
Cameron Gutman e27129fc48 Add the app name to the shortcut trampoline 2019-07-16 22:32:37 -07:00
Cameron Gutman d54fdc9f5f Refactor shortcut and channel code and handle removal of apps and PCs properly 2019-07-16 22:16:29 -07:00
Cameron Gutman dc984e8679 Fix duplicate programs when starting games 2019-07-16 21:29:02 -07:00
Cameron Gutman ee46906376 Fix splitting of address string 2019-07-16 20:36:36 -07:00
Cameron Gutman 1d76536e31 Delete PCs by UUID instead of name 2019-07-16 20:35:18 -07:00
Cameron Gutman dc97adc7a1 Fix upgrading from a build prior to cert pinning support 2019-07-16 20:08:41 -07:00
Cameron Gutman a1c659b7b8 Add support for IPv6-only hosts 2019-07-15 01:28:23 -07:00
Cameron Gutman 27f0fd63b3 Add support for IPv6-only mDNS 2019-07-14 14:17:39 -07:00
Cameron Gutman 83b66b19de Add support for zero configuration IPv6 streaming 2019-07-14 00:21:13 -07:00
Cameron Gutman ba0171221c Upgrade BouncyCastyle to 1.62 2019-07-13 23:57:21 -07:00
Cameron Gutman 6fa1c35521 Merge pull request #718 from uniqx/store-metadata-de
german fdroid store listing translation
2019-07-12 18:10:00 -07:00
Cameron Gutman 7a3fbd8dae Merge pull request #715 from kevinxucs/kevinxucs/update-locales
Translate some of the zh-rCN strings
2019-07-12 18:09:27 -07:00
Michael Pöhn 329ee1a0bc german fdroid store listing translation 2019-07-12 10:44:45 +02:00
Kaiwen Xu 11908e07bf Translate some of the zh-rCN strings. 2019-07-12 01:27:20 -07:00
Cameron Gutman fd53122cb3 Create the PC channel on pairing and add each app to it upon launch 2019-07-12 00:23:13 -07:00
Cameron Gutman d9c0830198 Merge branch 'tv-channels' of https://github.com/GinVavilon/moonlight-android into GinVavilon-tv-channels 2019-07-11 19:19:15 -07:00
Cameron Gutman d0aafb3814 Add Windows to PC requirements 2019-07-10 22:15:58 -07:00
Cameron Gutman 40a3cc2ecb Tweak on-screen overlay a bit 2019-07-10 20:55:01 -07:00
Cameron Gutman 4469013bb5 Merge pull request #716 from kevinxucs/kevinxucs/stats-overlay
Implement performance stats overlay
2019-07-10 20:36:22 -07:00
Cameron Gutman 78393932d0 Update to AGP 3.4.2 2019-07-10 20:13:02 -07:00
Cameron Gutman dbc2491151 Don't manually specify a build tools version 2019-07-10 20:12:41 -07:00
Kaiwen Xu 01eb7a2b64 Add executable permission to gradlew scripts. 2019-07-08 01:12:36 -07:00
Kaiwen Xu 252285e4f7 Implement performance overlay. 2019-07-08 00:55:25 -07:00
GinVavilon df7333b8d0 Add channels support for the Android TV (Oreo) 2019-07-07 22:25:31 +03:00
Cameron Gutman cf98ec2c41 Fix streaming on older servers 2019-07-05 21:29:10 -07:00
Cameron Gutman 754773420f Generate SHA-256 client certificates instead of SHA-1 2019-07-05 21:23:18 -07:00
Cameron Gutman 6574a0aab2 Fix codec blacklisting 2019-07-02 23:20:14 -07:00
Cameron Gutman 5d4988969e Fix layout of Fastlane metadata 2019-06-29 22:48:28 -07:00
Cameron Gutman 5121eb1852 Add icon to metadata 2019-06-29 22:41:13 -07:00
Cameron Gutman 004aeef2a7 Initial Fastlane metadata for F-Droid 2019-06-29 22:11:35 -07:00
Cameron Gutman aa65a0312a Update moonlight-common with some minor cleanup 2019-06-26 17:40:49 -07:00
Cameron Gutman 1308a4ed80 Fix a user-reported crash 2019-06-22 22:01:30 -07:00
Cameron Gutman deb78e1c64 Version 7.4 2019-06-05 23:02:06 -07:00
Cameron Gutman 9aec6b1d31 Target API 29 2019-06-05 22:59:39 -07:00
Cameron Gutman 97702b8861 Fix mouse capture after returning focus to the window on Android Q 2019-06-05 22:43:16 -07:00
Cameron Gutman 832e7197c5 Delay a bit before reporting USB devices to allow the old InputDevice to go away 2019-06-05 22:26:06 -07:00
Cameron Gutman 26b992726c Use transparent status bar and navigation bar on Android Q 2019-06-05 21:50:03 -07:00
Cameron Gutman 1cb3588841 Use low latency WifiLock on Android Q 2019-06-05 21:09:55 -07:00
Cameron Gutman b461d546d6 Use new MediaCodecInfo helper to blacklist software codecs 2019-06-05 21:05:33 -07:00
Cameron Gutman b7810d6eb6 Use the newly public InputDevice.isExternal() function on Android Q 2019-06-05 20:23:22 -07:00
Cameron Gutman 6fb3a8e57d Build with the Android Q SDK 2019-06-05 20:21:19 -07:00
Cameron Gutman b521c784bc Version 7.3.1 2019-05-27 01:48:00 -07:00
twboyii 8e1641af5f Add untranslated string in zh-rTW (#701) 2019-05-27 01:47:10 -07:00
Cameron Gutman c0aac01d33 Update AGP to 3.4.1 2019-05-27 01:43:38 -07:00
Cameron Gutman 4f8b0adcbb Fix video on GFE 3.19 2019-05-27 01:42:39 -07:00
Cameron Gutman 393a4c9c8a Fix pointer capture on Android Q Beta 3 2019-05-16 21:27:01 -07:00
Cameron Gutman 99b53f9a6a Version 7.3 2019-05-07 20:53:12 -07:00
Cameron Gutman 8da563b280 Bound queued audio data to prevent excessive latency 2019-05-07 20:39:45 -07:00
Cameron Gutman d5b950e5cf Version 7.2.1 2019-05-01 20:14:21 -07:00
Cameron Gutman c46b9acf6b Update common to fix receive time 2019-04-30 23:19:19 -07:00
Cameron Gutman d8e322bac9 Sync PC offline icon with Moonlight Qt 2019-04-30 22:27:22 -07:00
Cameron Gutman 44871626cf Version 7.2 2019-04-27 22:11:02 -07:00
Cameron Gutman f661522b5d Update moonlight-common with additional perf improvements 2019-04-27 22:00:27 -07:00
Cameron Gutman a454b0ab78 Update moonlight-common with perf improvements 2019-04-26 18:37:27 -07:00
Cameron Gutman 75bf84d0d9 Update Gradle for AS 3.4 2019-04-26 18:34:16 -07:00
Cameron Gutman c248994ed4 Version 7.1 2019-04-07 14:09:58 -07:00
Cameron Gutman a7a34ec629 Update vibration weights to match Moonlight Qt 2019-04-06 01:02:03 -07:00
Cameron Gutman 8d469c5d0a Add on-screen connection warnings 2019-04-06 00:56:45 -07:00
Cameron Gutman e6979d50b5 Update AGP to 3.3.2 2019-04-06 00:48:40 -07:00
Cameron Gutman 6e25b135a3 Update ProGuard rules to avoid slf4j warnings 2019-03-20 18:57:40 -07:00
Cameron Gutman 04e093a2c2 Update moonlight-common 2019-03-20 18:51:08 -07:00
bubuleur 813f2edd95 Update French Language (#676) 2019-03-02 20:01:21 -08:00
Cameron Gutman 337d753a33 Reduce gamepad deadzone to 7% 2019-03-02 17:23:01 -08:00
Cameron Gutman 1137c74f76 Pass AudioAttributes on L+ when vibrating 2019-03-02 17:20:39 -08:00
Cameron Gutman 0c1451f757 Improve scaling of lock icon by increasing dimensions 2019-02-18 20:46:34 -08:00
Cameron Gutman 5ab9ea48fd Version 7.0.1 2019-02-17 18:20:40 -08:00
Cameron Gutman ffcb623040 Fix crash when a rumble effect only uses the high-frequency motor 2019-02-17 18:18:00 -08:00
Cameron Gutman bfe6929642 Version 7.0 2019-02-16 19:44:45 -08:00
Cameron Gutman 50d45011a8 Add device vibration and other fixes 2019-02-16 19:13:01 -08:00
Cameron Gutman 2f7087d6d3 Stop vibration on stream end 2019-02-16 18:05:08 -08:00
Cameron Gutman 92b71588d0 Implement rumble on Android InputDevice 2019-02-16 17:56:34 -08:00
Cameron Gutman 4f3d018764 Fix OSC colliding with player 1 2019-02-16 17:29:05 -08:00
Cameron Gutman a22e33eeb9 Add rumble support for the in-app Xbox driver 2019-02-16 17:03:10 -08:00
Cameron Gutman 6a939e7495 Don't display the termination dialog for intended terminations 2019-02-10 02:28:11 -08:00
Cameron Gutman f8ba7cf190 Update common with SOPS fixes 2019-02-09 20:59:59 -08:00
82 changed files with 1760 additions and 382 deletions
+3
View File
@@ -38,3 +38,6 @@ build/
# Compiled JNI libraries folder
**/jniLibs
app/.externalNativeBuild/
# NDK stuff
.cxx/
+15
View File
@@ -0,0 +1,15 @@
language: android
dist: trusty
git:
depth: 1
android:
components:
- tools
- platform-tools
- build-tools-29.0.1
- android-29
install:
- yes | sdkmanager "ndk-bundle"
+3 -1
View File
@@ -1,5 +1,7 @@
# Moonlight Android
[![Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-android.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-android)
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
@@ -17,7 +19,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for mo
## Installation
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/com.limelight/), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
## Requirements
+7 -7
View File
@@ -1,15 +1,14 @@
apply plugin: 'com.android.application'
android {
buildToolsVersion '28.0.3'
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
targetSdkVersion 29
versionName "6.2"
versionCode = 186
versionName "8.1"
versionCode = 198
}
flavorDimensions "root"
@@ -44,6 +43,7 @@ android {
lintOptions {
disable 'MissingTranslation'
lintConfig file("lint.xml")
}
bundle {
@@ -114,8 +114,8 @@ android {
}
dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
implementation 'org.bouncycastle:bcprov-jdk15on:1.62'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.62'
implementation 'org.jcodec:jcodec:0.2.3'
implementation project(':moonlight-common')
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="InvalidPackage">
<ignore path="**/bcpkix-jdk15on-*.jar"/>
</issue>
</lint>
+1
View File
@@ -25,3 +25,4 @@
# jMDNS
-dontwarn javax.jmdns.impl.DNSCache
-dontwarn org.slf4j.**
+10 -1
View File
@@ -4,9 +4,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
<uses-feature
android:name="android.hardware.touchscreen"
@@ -38,8 +41,14 @@
android:appCategory="game"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:installLocation="auto"
android:theme="@style/AppTheme">
<provider
android:name=".PosterContentProvider"
android:authorities="poster.${applicationId}"
android:enabled="true"
android:exported="true">
</provider>
<!-- Samsung multi-window support -->
<uses-library
android:name="com.sec.android.app.multiwindow"
+8 -6
View File
@@ -68,6 +68,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID";
public final static String NEW_PAIR_EXTRA = "NewPair";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
@@ -89,6 +90,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
return;
}
// Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper.createAppViewShortcut(computer, true, getIntent().getBooleanExtra(NEW_PAIR_EXTRA, false));
shortcutHelper.reportComputerShortcutUsed(computer);
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
@@ -181,7 +186,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override
public void run() {
// Disable shortcuts referencing this PC for now
shortcutHelper.disableShortcut(details.uuid,
shortcutHelper.disableComputerShortcut(details,
getResources().getString(R.string.scut_not_paired));
// Display a toast to the user and quit the activity
@@ -267,10 +272,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
setTitle(computerName);
label.setText(computerName);
// Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
shortcutHelper.reportShortcutUsed(uuidString);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
@@ -419,7 +420,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
case CREATE_SHORTCUT_ID:
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
if (!shortcutHelper.createPinnedGameShortcut(computer, app.app, appBits)) {
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
}
return true;
@@ -515,6 +516,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
// This app was removed in the latest app list
if (!foundExistingApp) {
shortcutHelper.disableAppShortcut(computer, existingApp.app, "App removed from PC");
appGridAdapter.removeApp(existingApp);
updated = true;
+163 -30
View File
@@ -13,9 +13,11 @@ import com.limelight.binding.input.virtual_controller.VirtualController;
import com.limelight.binding.video.CrashListener;
import com.limelight.binding.video.MediaCodecDecoderRenderer;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.binding.video.PerfOverlayListener;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
@@ -66,17 +68,20 @@ import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Locale;
public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
PerfOverlayListener
{
private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE;
@@ -111,12 +116,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean grabComboDown = false;
private StreamView streamView;
private boolean isHidingOverlays;
private TextView notificationOverlayView;
private int requestedNotificationOverlayVisibility = View.GONE;
private TextView performanceOverlayView;
private ShortcutHelper shortcutHelper;
private MediaCodecDecoderRenderer decoderRenderer;
private boolean reportedCrash;
private WifiManager.WifiLock wifiLock;
private WifiManager.WifiLock highPerfWifiLock;
private WifiManager.WifiLock lowLatencyWifiLock;
private boolean connectedToUsbDriverService = false;
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
@@ -201,6 +212,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.setOnTouchListener(this);
streamView.setInputCallbacks(this);
notificationOverlayView = findViewById(R.id.notificationOverlay);
performanceOverlayView = findViewById(R.id.performanceOverlay);
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -223,9 +238,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
wifiLock.setReferenceCounted(false);
wifiLock.acquire();
highPerfWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Moonlight High Perf Lock");
highPerfWifiLock.setReferenceCounted(false);
highPerfWifiLock.acquire();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
lowLatencyWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Moonlight Low Latency Lock");
lowLatencyWifiLock.setReferenceCounted(false);
lowLatencyWifiLock.acquire();
}
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
@@ -251,10 +272,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return;
}
// Add a launcher shortcut for this PC (forced, since this is user interaction)
// Report this shortcut being used
ComputerDetails computer = new ComputerDetails();
computer.name = pcName;
computer.uuid = uuid;
shortcutHelper = new ShortcutHelper(this);
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
shortcutHelper.reportShortcutUsed(uuid);
shortcutHelper.reportComputerShortcutUsed(computer);
if (appName != null) {
// This may be null if launched from the "Resume Session" PC context menu item
shortcutHelper.reportGameLaunched(computer, new NvApp(appName, appId, willStreamHdr));
}
// Initialize the MediaCodec helper before creating the decoder
GlPreferences glPrefs = GlPreferences.readPreferences(this);
@@ -274,10 +301,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false;
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
LimeLog.info("Display supports HDR10");
foundHdr10 = true;
if (hdrCaps != null) {
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
foundHdr10 = true;
}
}
}
@@ -296,7 +325,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
willStreamHdr = false;
}
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig,
// Check if the user has enabled performance stats overlay
if (prefConfig.enablePerfOverlay) {
performanceOverlayView.setVisibility(View.VISIBLE);
}
decoderRenderer = new MediaCodecDecoderRenderer(
this,
prefConfig,
new CrashListener() {
@Override
public void notifyCrash(Exception e) {
@@ -311,8 +347,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
tombstonePrefs.getInt("CrashCount", 0),
connMgr.isActiveNetworkMetered(),
willStreamHdr,
glPrefs.glRenderer
);
glPrefs.glRenderer,
this);
// Don't stream HDR if the decoder can't support it
if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) {
@@ -386,7 +422,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(new NvApp(appName, appId, willStreamHdr))
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
.setBitrate(prefConfig.bitrate)
.setEnableSops(prefConfig.enableSops)
.enableLocalAudioPlayback(prefConfig.playHostAudio)
@@ -424,7 +460,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (prefConfig.onscreenController) {
// create virtual onscreen controller
virtualController = new VirtualController(conn,
virtualController = new VirtualController(controllerHandler,
(FrameLayout)streamView.getParent(),
this);
virtualController.refreshLayout();
@@ -460,15 +496,34 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (virtualController != null) {
// Refresh layout of OSC for possible new screen size
virtualController.refreshLayout();
}
// Hide OSC in PiP
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (isInPictureInPictureMode()) {
// Hide on-screen overlays in PiP mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (isInPictureInPictureMode()) {
isHidingOverlays = true;
if (virtualController != null) {
virtualController.hide();
}
else {
performanceOverlayView.setVisibility(View.GONE);
notificationOverlayView.setVisibility(View.GONE);
}
else {
isHidingOverlays = false;
// Restore overlays to previous state when leaving PiP
if (virtualController != null) {
virtualController.show();
}
if (prefConfig.enablePerfOverlay) {
performanceOverlayView.setVisibility(View.VISIBLE);
}
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);
}
}
}
@@ -505,8 +560,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Capture is lost when focus is lost, so it must be requested again
// when focus is regained.
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
// Recapture the pointer if focus was regained
streamView.requestPointerCapture();
// Recapture the pointer if focus was regained. On Android Q,
// we have to delay a bit before requesting capture because otherwise
// we'll hit the "requestPointerCapture called for a window that has no focus"
// error and it will not actually capture the cursor.
Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
streamView.requestPointerCapture();
}
}, 500);
}
}
}
@@ -697,7 +761,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
inputManager.unregisterInputDeviceListener(controllerHandler);
}
wifiLock.release();
if (lowLatencyWifiLock != null) {
lowLatencyWifiLock.release();
}
if (highPerfWifiLock != null) {
highPerfWifiLock.release();
}
if (connectedToUsbDriverService) {
// Unbind from the discovery service
@@ -1267,6 +1336,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (connecting || connected) {
connecting = connected = false;
controllerHandler.stop();
// Stop may take a few hundred ms to do some network I/O to tell
// the server we're going away and clean up. Let it run in a separate
// thread to keep things smooth for the UI. Inside moonlight-common,
@@ -1322,8 +1393,45 @@ public class Game extends Activity implements SurfaceHolder.Callback,
LimeLog.severe("Connection terminated: " + errorCode);
stopConnection();
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
// Display the error dialog if it was an unexpected termination.
// Otherwise, just finish the activity immediately.
if (errorCode != 0) {
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
else {
finish();
}
}
}
});
}
@Override
public void connectionStatusUpdate(final int connectionStatus) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (prefConfig.disableWarnings) {
return;
}
if (connectionStatus == MoonBridge.CONN_STATUS_POOR) {
if (prefConfig.bitrate > 5000) {
notificationOverlayView.setText(getResources().getString(R.string.slow_connection_msg));
}
else {
notificationOverlayView.setText(getResources().getString(R.string.poor_connection_msg));
}
requestedNotificationOverlayVisibility = View.VISIBLE;
}
else if (connectionStatus == MoonBridge.CONN_STATUS_OKAY) {
requestedNotificationOverlayVisibility = View.GONE;
}
if (!isHidingOverlays) {
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);
}
}
});
@@ -1342,10 +1450,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
connected = true;
connecting = false;
// Hide the mouse cursor now. Doing it before
// dismissing the spinner seems to be undone
// when the spinner gets displayed.
inputCaptureProvider.enableCapture();
// Hide the mouse cursor now after a short delay.
// Doing it before dismissing the spinner seems to be undone
// when the spinner gets displayed. On Android Q, even now
// is too early to capture. We will delay a second to allow
// the spinner to dismiss before capturing.
Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
inputCaptureProvider.enableCapture();
}
}, 500);
// Keep the display on
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -1377,6 +1493,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
@Override
public void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
LimeLog.info(String.format((Locale)null, "Rumble on gamepad %d: %04x %04x", controllerNumber, lowFreqMotor, highFreqMotor));
controllerHandler.handleRumble(controllerNumber, lowFreqMotor, highFreqMotor);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (!surfaceCreated) {
@@ -1496,4 +1619,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
hideSystemUi(2000);
}
}
@Override
public void onPerfUpdate(final String text) {
runOnUiThread(new Runnable() {
@Override
public void run() {
performanceOverlayView.setText(text);
}
});
}
}
+12 -7
View File
@@ -9,6 +9,7 @@ import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter;
import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
@@ -440,7 +441,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (toastSuccess) {
// Open the app list after a successful pairing attempt
doAppList(computer);
doAppList(computer, true);
}
else {
// Start polling again if we're still in the foreground
@@ -539,7 +540,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}).start();
}
private void doAppList(ComputerDetails computer) {
private void doAppList(ComputerDetails computer, boolean newlyPaired) {
if (computer.state == ComputerDetails.State.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
@@ -552,6 +553,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
startActivity(i);
}
@@ -584,14 +586,13 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
}
managerBinder.removeComputer(computer.details.name);
removeComputer(computer.details);
}
}, null);
return true;
case APP_LIST_ID:
doAppList(computer.details);
doAppList(computer.details, false);
return true;
case RESUME_ID:
@@ -629,12 +630,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
private void removeComputer(ComputerDetails details) {
managerBinder.removeComputer(details);
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
if (details.equals(computer.details)) {
// Disable or delete shortcuts referencing this PC
shortcutHelper.disableShortcut(details.uuid,
shortcutHelper.disableComputerShortcut(details,
getResources().getString(R.string.scut_deleted_pc));
pcGridAdapter.removeComputer(computer);
@@ -665,7 +670,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Add a launcher shortcut for this PC
if (details.pairState == PairState.PAIRED) {
shortcutHelper.createAppViewShortcut(details.uuid, details, false);
shortcutHelper.createAppViewShortcutForOnlineHost(details);
}
if (existingEntry != null) {
@@ -707,7 +712,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details);
doAppList(computer.details, false);
}
}
});
@@ -0,0 +1,107 @@
package com.limelight;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import com.limelight.grid.assets.DiskAssetLoader;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.List;
public class PosterContentProvider extends ContentProvider {
public static final String AUTHORITY = "poster." + BuildConfig.APPLICATION_ID;
public static final String PNG_MIME_TYPE = "image/png";
public static final int APP_ID_PATH_INDEX = 2;
public static final int COMPUTER_UUID_PATH_INDEX = 1;
private DiskAssetLoader mDiskAssetLoader;
private static final UriMatcher sUriMatcher;
private static final String BOXART_PATH = "boxart";
private static final int BOXART_URI_ID = 1;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, BOXART_PATH, BOXART_URI_ID);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
int match = sUriMatcher.match(uri);
if (match == BOXART_URI_ID) {
return openBoxArtFile(uri, mode);
}
return openBoxArtFile(uri, mode);
}
public ParcelFileDescriptor openBoxArtFile(Uri uri, String mode) throws FileNotFoundException {
if (!"r".equals(mode)) {
throw new UnsupportedOperationException("This provider is only for read mode");
}
List<String> segments = uri.getPathSegments();
if (segments.size() != 3) {
throw new FileNotFoundException();
}
String appId = segments.get(APP_ID_PATH_INDEX);
String uuid = segments.get(COMPUTER_UUID_PATH_INDEX);
File file = mDiskAssetLoader.getFile(uuid, Integer.parseInt(appId));
if (file.exists()) {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("This provider is only for read mode");
}
@Override
public String getType(Uri uri) {
return PNG_MIME_TYPE;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("This provider is only for read mode");
}
@Override
public boolean onCreate() {
mDiskAssetLoader = new DiskAssetLoader(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
throw new UnsupportedOperationException("This provider doesn't support query");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException("This provider is support read only");
}
public static Uri createBoxArtUri(String uuid, String appId) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.appendPath(BOXART_PATH)
.appendPath(uuid)
.appendPath(appId)
.build();
}
}
@@ -23,14 +23,12 @@ import java.util.UUID;
public class ShortcutTrampoline extends Activity {
private String uuidString;
private String appIdString;
private NvApp app;
private ArrayList<Intent> intentStack = new ArrayList<>();
private ComputerDetails computer;
private SpinnerDialog blockingLoadSpinner;
public final static String APP_ID_EXTRA = "AppId";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
@@ -101,10 +99,9 @@ public class ShortcutTrampoline extends Activity {
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
// Launch game if provided app ID, otherwise launch app view
if (appIdString != null && appIdString.length() > 0) {
if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder));
if (app != null) {
if (details.runningGameId == 0 || details.runningGameId == app.getAppId()) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder));
// Close this activity
finish();
@@ -114,8 +111,7 @@ public class ShortcutTrampoline extends Activity {
} else {
// Create the start intent immediately, so we can safely unbind the managerBinder
// below before we return.
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder);
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder);
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
@Override
@@ -155,7 +151,7 @@ public class ShortcutTrampoline extends Activity {
// If a game is running, we'll make the stream the top level activity
if (details.runningGameId != 0) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", details.runningGameId, false), details, managerBinder));
new NvApp(null, details.runningGameId, false), details, managerBinder));
}
// Now start the activities
@@ -198,7 +194,7 @@ public class ShortcutTrampoline extends Activity {
}
};
protected boolean validateInput() {
protected boolean validateInput(String uuidString, String appIdString) {
// Validate UUID
if (uuidString == null) {
Dialog.displayDialog(ShortcutTrampoline.this,
@@ -240,10 +236,16 @@ public class ShortcutTrampoline extends Activity {
UiHelper.notifyNewRootView(this);
String appIdString = getIntent().getStringExtra(Game.EXTRA_APP_ID);
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
if (validateInput()) {
if (validateInput(uuidString, appIdString)) {
if (appIdString != null && !appIdString.isEmpty()) {
app = new NvApp(getIntent().getStringExtra(Game.EXTRA_APP_NAME),
Integer.parseInt(appIdString),
getIntent().getBooleanExtra(Game.EXTRA_APP_HDR, false));
}
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
@@ -168,8 +168,17 @@ public class AndroidAudioRenderer implements AudioRenderer {
}
@Override
public void playDecodedAudio(byte[] audioData) {
track.write(audioData, 0, audioData.length);
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) {
// 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());
}
}
@Override
@@ -148,7 +148,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
@@ -4,8 +4,11 @@ import android.content.Context;
import android.hardware.input.InputManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.media.AudioAttributes;
import android.os.Build;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.InputEvent;
@@ -14,6 +17,7 @@ import android.view.MotionEvent;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.binding.input.driver.AbstractController;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.nvstream.NvConnection;
@@ -51,6 +55,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final double stickDeadzone;
private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
private final Vibrator deviceVibrator;
private boolean hasGameController;
private final PreferenceConfiguration prefConfig;
@@ -61,10 +66,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
this.conn = conn;
this.gestures = gestures;
this.prefConfig = prefConfig;
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// HACK: For now we're hardcoding a 7% deadzone. Some deadzone
// is required for controller batching support to work.
int deadzonePercentage = 10;
int deadzonePercentage = 7;
int[] ids = InputDevice.getDeviceIds();
for (int id : ids) {
@@ -151,6 +157,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
onInputDeviceAdded(deviceId);
}
public void stop() {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.vibrator != null) {
deviceContext.vibrator.cancel();
}
}
deviceVibrator.cancel();
}
private static boolean hasJoystickAxes(InputDevice device) {
return (device.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_X) != null &&
@@ -206,6 +224,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
if (PreferenceConfiguration.readPreferences(context).onscreenController) {
LimeLog.info("Counting OSC gamepad");
mask |= 1;
}
LimeLog.info("Enumerated "+count+" gamepads");
return mask;
}
@@ -298,10 +321,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.assignedControllerNumber = true;
}
private UsbDeviceContext createUsbDeviceContextForDevice(int deviceId) {
private UsbDeviceContext createUsbDeviceContextForDevice(AbstractController device) {
UsbDeviceContext context = new UsbDeviceContext();
context.id = deviceId;
context.id = device.getControllerId();
context.device = device;
context.leftStickDeadzoneRadius = (float) stickDeadzone;
context.rightStickDeadzoneRadius = (float) stickDeadzone;
@@ -311,17 +335,23 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
private static boolean isExternal(InputDevice dev) {
try {
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
return dev.isExternal();
}
else {
try {
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
}
}
// Answer true if we don't know
@@ -393,6 +423,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.name = devName;
context.id = dev.getId();
if (dev.getVibrator().hasVibrator()) {
context.vibrator = dev.getVibrator();
}
context.leftStickXAxis = MotionEvent.AXIS_X;
context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
@@ -614,7 +648,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private short getActiveControllerMask() {
if (prefConfig.multiController) {
return (short)(currentControllers | initialControllers);
return (short)(currentControllers | initialControllers | (prefConfig.onscreenController ? 1 : 0));
}
else {
// Only Player 1 is active with multi-controller disabled
@@ -1043,6 +1077,94 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
private void rumbleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) {
// Since we can only use a single amplitude value, compute the desired amplitude
// by taking 80% of the big motor and 33% of the small motor, then capping to 255.
// NB: This value is now 0-255 as required by VibrationEffect.
short lowFreqMotorMSB = (short)((lowFreqMotor >> 8) & 0xFF);
short highFreqMotorMSB = (short)((highFreqMotor >> 8) & 0xFF);
int simulatedAmplitude = Math.min(255, (int)((lowFreqMotorMSB * 0.80) + (highFreqMotorMSB * 0.33)));
if (simulatedAmplitude == 0) {
// This case is easy - just cancel the current effect and get out.
// NB: We cannot simply check lowFreqMotor == highFreqMotor == 0
// because our simulatedAmplitude could be 0 even though our inputs
// are not (ex: lowFreqMotor == 0 && highFreqMotor == 1).
vibrator.cancel();
return;
}
// Attempt to use amplitude-based control if we're on Oreo and the device
// supports amplitude-based vibration control.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (vibrator.hasAmplitudeControl()) {
VibrationEffect effect = VibrationEffect.createOneShot(60000, simulatedAmplitude);
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.build();
vibrator.vibrate(effect, audioAttributes);
return;
}
}
// If we reach this point, we don't have amplitude controls available, so
// we must emulate it by PWMing the vibration. Ick.
long pwmPeriod = 20;
long onTime = (long)((simulatedAmplitude / 255.0) * pwmPeriod);
long offTime = pwmPeriod - onTime;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.build();
vibrator.vibrate(new long[]{0, onTime, offTime}, 0, audioAttributes);
}
else {
vibrator.vibrate(new long[]{0, onTime, offTime}, 0);
}
}
public void handleRumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
boolean foundMatchingDevice = false;
boolean vibrated = false;
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
foundMatchingDevice = true;
if (deviceContext.vibrator != null) {
vibrated = true;
rumbleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor);
}
}
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
foundMatchingDevice = vibrated = true;
deviceContext.device.rumble((short)lowFreqMotor, (short)highFreqMotor);
}
}
// We may decide to rumble the device for player 1
if (controllerNumber == 0) {
// If we didn't find a matching device, it must be the on-screen
// controls that triggered the rumble. Vibrate the device if
// the user has requested that behavior.
if (!foundMatchingDevice && prefConfig.onscreenController && !prefConfig.onlyL3R3 && prefConfig.vibrateOsc) {
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
}
else if (foundMatchingDevice && !vibrated && prefConfig.vibrateFallbackToDevice) {
// We found a device to vibrate but it didn't have rumble support. The user
// has requested us to vibrate the device in this case.
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
}
}
}
public boolean handleButtonUp(KeyEvent event) {
InputDeviceContext context = getContextForEvent(event);
if (context == null) {
@@ -1294,12 +1416,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return true;
}
public void reportOscState(short buttonFlags,
short leftStickX, short leftStickY,
short rightStickX, short rightStickY,
byte leftTrigger, byte rightTrigger) {
defaultContext.leftStickX = leftStickX;
defaultContext.leftStickY = leftStickY;
defaultContext.rightStickX = rightStickX;
defaultContext.rightStickY = rightStickY;
defaultContext.leftTrigger = leftTrigger;
defaultContext.rightTrigger = rightTrigger;
defaultContext.inputMap = buttonFlags;
sendControllerInputPacket(defaultContext);
}
@Override
public void reportControllerState(int controllerId, short buttonFlags,
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger) {
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
GenericControllerContext context = usbDeviceContexts.get(controllerId);
if (context == null) {
return;
}
@@ -1334,19 +1474,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
@Override
public void deviceRemoved(int controllerId) {
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
public void deviceRemoved(AbstractController controller) {
UsbDeviceContext context = usbDeviceContexts.get(controller.getControllerId());
if (context != null) {
LimeLog.info("Removed controller: "+controllerId);
LimeLog.info("Removed controller: "+controller.getControllerId());
releaseControllerNumber(context);
usbDeviceContexts.remove(controllerId);
usbDeviceContexts.remove(controller.getControllerId());
}
}
@Override
public void deviceAdded(int controllerId) {
UsbDeviceContext context = createUsbDeviceContextForDevice(controllerId);
usbDeviceContexts.put(controllerId, context);
public void deviceAdded(AbstractController controller) {
UsbDeviceContext context = createUsbDeviceContextForDevice(controller);
usbDeviceContexts.put(controller.getControllerId(), context);
}
class GenericControllerContext {
@@ -1375,6 +1515,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
class InputDeviceContext extends GenericControllerContext {
public String name;
public Vibrator vibrator;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
@@ -1412,5 +1553,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public long startDownTime = 0;
}
class UsbDeviceContext extends GenericControllerContext {}
class UsbDeviceContext extends GenericControllerContext {
public AbstractController device;
}
}
@@ -37,11 +37,13 @@ public abstract class AbstractController {
this.listener = listener;
}
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
protected void notifyDeviceRemoved() {
listener.deviceRemoved(deviceId);
listener.deviceRemoved(this);
}
protected void notifyDeviceAdded() {
listener.deviceAdded(deviceId);
listener.deviceAdded(this);
}
}
@@ -30,6 +30,18 @@ public abstract class AbstractXboxController extends AbstractController {
private Thread createInputThread() {
return new Thread() {
public void run() {
try {
// Delay for a moment before reporting the new gamepad and
// accepting new input. This allows time for the old InputDevice
// to go away before we reclaim its spot. If the old device is still
// around when we call notifyDeviceAdded(), we won't be able to claim
// the controller number used by the original InputDevice.
Thread.sleep(1000);
} catch (InterruptedException e) {}
// Report that we're added _before_ reporting input
notifyDeviceAdded();
while (!isInterrupted() && !stopped) {
byte[] buffer = new byte[64];
@@ -114,9 +126,6 @@ public abstract class AbstractXboxController extends AbstractController {
return false;
}
// Report that we're added _before_ starting the input thread
notifyDeviceAdded();
// Start listening for controller input
inputThread = createInputThread();
inputThread.start();
@@ -131,6 +140,9 @@ public abstract class AbstractXboxController extends AbstractController {
stopped = true;
// Cancel any rumble effects
rumble((short)0, (short)0);
// Stop the input thread
if (inputThread != null) {
inputThread.interrupt();
@@ -6,6 +6,6 @@ public interface UsbDriverListener {
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger);
void deviceRemoved(int controllerId);
void deviceAdded(int controllerId);
void deviceRemoved(AbstractController controller);
void deviceAdded(AbstractController controller);
}
@@ -47,26 +47,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
@Override
public void deviceRemoved(int controllerId) {
public void deviceRemoved(AbstractController controller) {
// Remove the the controller from our list (if not removed already)
for (AbstractController controller : controllers) {
if (controller.getControllerId() == controllerId) {
controllers.remove(controller);
break;
}
}
controllers.remove(controller);
// Call through to the client's listener
if (listener != null) {
listener.deviceRemoved(controllerId);
listener.deviceRemoved(controller);
}
}
@Override
public void deviceAdded(int controllerId) {
public void deviceAdded(AbstractController controller) {
// Call through to the client's listener
if (listener != null) {
listener.deviceAdded(controllerId);
listener.deviceAdded(controller);
}
}
@@ -113,7 +108,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
// Report all controllerMap that already exist
if (listener != null) {
for (AbstractController controller : controllers) {
listener.deviceAdded(controller.getControllerId());
listener.deviceAdded(controller);
}
}
}
@@ -139,4 +139,17 @@ public class Xbox360Controller extends AbstractXboxController {
// No need to fail init if the LED command fails
return true;
}
@Override
public void rumble(short lowFreqMotor, short highFreqMotor) {
byte[] data = {
0x00, 0x08, 0x00,
(byte)(lowFreqMotor >> 8), (byte)(highFreqMotor >> 8),
0x00, 0x00, 0x00
};
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
if (res != data.length) {
LimeLog.warning("Rumble transfer failed: "+res);
}
}
}
@@ -48,6 +48,7 @@ public class XboxOneController extends AbstractXboxController {
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT2),
};
private byte seqNum = 0;
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(device, connection, deviceId, listener);
@@ -134,8 +135,6 @@ public class XboxOneController extends AbstractXboxController {
@Override
protected boolean doInit() {
byte seqNum = 0;
// Send all applicable init packets
for (InitPacket pkt : INIT_PKTS) {
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
@@ -162,6 +161,20 @@ public class XboxOneController extends AbstractXboxController {
return true;
}
@Override
public void rumble(short lowFreqMotor, short highFreqMotor) {
byte[] data = {
0x09, 0x00, seqNum++, 0x09, 0x00,
0x0F, 0x00, 0x00,
(byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9),
(byte)0xFF, 0x00, (byte)0xFF
};
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
if (res != data.length) {
LimeLog.warning("Rumble transfer failed: "+res);
}
}
private static class InitPacket {
final int vendorId;
final int productId;
@@ -13,6 +13,7 @@ import android.widget.RelativeLayout;
import android.widget.Toast;
import com.limelight.R;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.nvstream.NvConnection;
import java.util.ArrayList;
@@ -38,7 +39,7 @@ public class VirtualController {
private static final boolean _PRINT_DEBUG_INFORMATION = false;
private NvConnection connection = null;
private ControllerHandler controllerHandler;
private Context context = null;
private FrameLayout frame_layout = null;
@@ -53,8 +54,8 @@ public class VirtualController {
private List<VirtualControllerElement> elements = new ArrayList<>();
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
this.connection = conn;
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
this.controllerHandler = controllerHandler;
this.frame_layout = layout;
this.context = context;
@@ -173,15 +174,15 @@ public class VirtualController {
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
if (connection != null) {
connection.sendControllerInput(
if (controllerHandler != null) {
controllerHandler.reportOscState(
inputContext.inputMap,
inputContext.leftTrigger,
inputContext.rightTrigger,
inputContext.leftStickX,
inputContext.leftStickY,
inputContext.rightStickX,
inputContext.rightStickY
inputContext.rightStickY,
inputContext.leftTrigger,
inputContext.rightTrigger
);
}
}
@@ -8,10 +8,12 @@ import org.jcodec.codecs.h264.io.model.SeqParameterSet;
import org.jcodec.codecs.h264.io.model.VUIParameters;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.jni.MoonBridge;
import com.limelight.preferences.PreferenceConfiguration;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
@@ -38,6 +40,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private boolean submittedCsd;
private boolean submitCsdNextCall;
private Context context;
private MediaCodec videoDecoder;
private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4;
@@ -55,6 +58,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private String glRenderer;
private boolean foreground = true;
private boolean legacyFrameDropRendering = false;
private PerfOverlayListener perfListener;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
@@ -63,13 +67,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private long initialExceptionTimestamp;
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
private VideoStats activeWindowVideoStats;
private VideoStats lastWindowVideoStats;
private VideoStats globalVideoStats;
private long lastTimestampUs;
private long decoderTimeMs;
private long totalTimeMs;
private int totalFramesReceived;
private int totalFramesRendered;
private int frameLossEvents;
private int framesLost;
private int lastFrameNumber;
private int refreshRate;
private PreferenceConfiguration prefs;
@@ -119,16 +121,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
this.renderTarget = renderTarget;
}
public MediaCodecDecoderRenderer(PreferenceConfiguration prefs,
public MediaCodecDecoderRenderer(Context context, PreferenceConfiguration prefs,
CrashListener crashListener, int consecutiveCrashCount,
boolean meteredData, boolean requestedHdr,
String glRenderer) {
String glRenderer, PerfOverlayListener perfListener) {
//dumpDecoders();
this.context = context;
this.prefs = prefs;
this.crashListener = crashListener;
this.consecutiveCrashCount = consecutiveCrashCount;
this.glRenderer = glRenderer;
this.perfListener = perfListener;
this.activeWindowVideoStats = new VideoStats();
this.lastWindowVideoStats = new VideoStats();
this.globalVideoStats = new VideoStats();
avcDecoder = findAvcDecoder();
if (avcDecoder != null) {
@@ -311,7 +319,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
if (USE_FRAME_RENDER_TIME) {
totalTimeMs += delta;
activeWindowVideoStats.totalTimeMs += delta;
}
}
}
@@ -421,14 +429,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoDecoder.releaseOutputBuffer(lastIndex, true);
}
totalFramesRendered++;
activeWindowVideoStats.totalFramesRendered++;
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
activeWindowVideoStats.decoderTimeMs += delta;
if (!USE_FRAME_RENDER_TIME) {
totalTimeMs += delta;
activeWindowVideoStats.totalTimeMs += delta;
}
}
} else {
@@ -585,17 +593,58 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return MoonBridge.DR_OK;
}
totalFramesReceived++;
// We can receive the same "frame" multiple times if it's an IDR frame.
// In that case, each frame start NALU is submitted independently.
if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
framesLost += frameNumber - lastFrameNumber - 1;
frameLossEvents++;
if (lastFrameNumber == 0) {
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
// We can receive the same "frame" multiple times if it's an IDR frame.
// In that case, each frame start NALU is submitted independently.
activeWindowVideoStats.framesLost += frameNumber - lastFrameNumber - 1;
activeWindowVideoStats.totalFrames += frameNumber - lastFrameNumber - 1;
activeWindowVideoStats.frameLossEvents++;
}
lastFrameNumber = frameNumber;
// Flip stats windows roughly every second
if (System.currentTimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) {
if (prefs.enablePerfOverlay) {
VideoStats lastTwo = new VideoStats();
lastTwo.add(lastWindowVideoStats);
lastTwo.add(activeWindowVideoStats);
VideoStatsFps fps = lastTwo.getFps();
String decoder;
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
decoder = avcDecoder.getName();
} else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
decoder = hevcDecoder.getName();
} else {
decoder = "(unknown)";
}
float decodeTimeMs = (float)lastTwo.decoderTimeMs / lastTwo.totalFramesReceived;
String perfText = context.getString(
R.string.perf_overlay_text,
initialWidth + "x" + initialHeight,
decoder,
fps.totalFps,
fps.receivedFps,
fps.renderedFps,
(float)lastTwo.framesLost / lastTwo.totalFrames * 100,
((float)lastTwo.totalTimeMs / lastTwo.totalFramesReceived) - decodeTimeMs,
decodeTimeMs);
perfListener.onPerfUpdate(perfText);
}
globalVideoStats.add(activeWindowVideoStats);
lastWindowVideoStats.copy(activeWindowVideoStats);
activeWindowVideoStats.clear();
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
}
activeWindowVideoStats.totalFramesReceived++;
activeWindowVideoStats.totalFrames++;
int inputBufferIndex;
ByteBuffer buf;
@@ -603,7 +652,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
if (!FRAME_RENDER_TIME_ONLY) {
// Count time from first packet received to decode start
totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
activeWindowVideoStats.totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
}
if (timestampUs <= lastTimestampUs) {
@@ -910,17 +959,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
public int getAverageEndToEndLatency() {
if (totalFramesReceived == 0) {
if (globalVideoStats.totalFramesReceived == 0) {
return 0;
}
return (int)(totalTimeMs / totalFramesReceived);
return (int)(globalVideoStats.totalTimeMs / globalVideoStats.totalFramesReceived);
}
public int getAverageDecoderLatency() {
if (totalFramesReceived == 0) {
if (globalVideoStats.totalFramesReceived == 0) {
return 0;
}
return (int)(decoderTimeMs / totalFramesReceived);
return (int)(globalVideoStats.decoderTimeMs / globalVideoStats.totalFramesReceived);
}
static class DecoderHungException extends RuntimeException {
@@ -981,9 +1030,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
str += "Total frames received: "+renderer.totalFramesReceived+"\n";
str += "Total frames rendered: "+renderer.totalFramesRendered+"\n";
str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\n";
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
@@ -474,6 +474,24 @@ public class MediaCodecHelper {
return null;
}
private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) {
// Use the new isSoftwareOnly() function on Android Q
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (codecInfo.isSoftwareOnly()) {
LimeLog.info("Skipping software-only decoder: "+codecInfo.getName());
return true;
}
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
return true;
}
return false;
}
public static MediaCodecInfo findFirstDecoder(String mimeType) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
@@ -482,15 +500,14 @@ public class MediaCodecHelper {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports the specified video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase(mimeType)) {
// Skip blacklisted codecs
if (isCodecBlacklisted(codecInfo)) {
continue;
}
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
@@ -530,17 +547,16 @@ public class MediaCodecHelper {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports the requested video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase(mimeType)) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
// Skip blacklisted codecs
if (isCodecBlacklisted(codecInfo)) {
continue;
}
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
if (requiredProfile != -1) {
@@ -0,0 +1,5 @@
package com.limelight.binding.video;
public interface PerfOverlayListener {
void onPerfUpdate(final String text);
}
@@ -0,0 +1,70 @@
package com.limelight.binding.video;
class VideoStats {
long decoderTimeMs;
long totalTimeMs;
int totalFrames;
int totalFramesReceived;
int totalFramesRendered;
int frameLossEvents;
int framesLost;
long measurementStartTimestamp;
void add(VideoStats other) {
this.decoderTimeMs += other.decoderTimeMs;
this.totalTimeMs += other.totalTimeMs;
this.totalFrames += other.totalFrames;
this.totalFramesReceived += other.totalFramesReceived;
this.totalFramesRendered += other.totalFramesRendered;
this.frameLossEvents += other.frameLossEvents;
this.framesLost += other.framesLost;
if (this.measurementStartTimestamp == 0) {
this.measurementStartTimestamp = other.measurementStartTimestamp;
}
assert other.measurementStartTimestamp <= this.measurementStartTimestamp;
}
void copy(VideoStats other) {
this.decoderTimeMs = other.decoderTimeMs;
this.totalTimeMs = other.totalTimeMs;
this.totalFrames = other.totalFrames;
this.totalFramesReceived = other.totalFramesReceived;
this.totalFramesRendered = other.totalFramesRendered;
this.frameLossEvents = other.frameLossEvents;
this.framesLost = other.framesLost;
this.measurementStartTimestamp = other.measurementStartTimestamp;
}
void clear() {
this.decoderTimeMs = 0;
this.totalTimeMs = 0;
this.totalFrames = 0;
this.totalFramesReceived = 0;
this.totalFramesRendered = 0;
this.frameLossEvents = 0;
this.framesLost = 0;
this.measurementStartTimestamp = 0;
}
VideoStatsFps getFps() {
float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000;
VideoStatsFps fps = new VideoStatsFps();
if (elapsed > 0) {
fps.totalFps = this.totalFrames / elapsed;
fps.receivedFps = this.totalFramesReceived / elapsed;
fps.renderedFps = this.totalFramesRendered / elapsed;
}
return fps;
}
}
class VideoStatsFps {
float totalFps;
float receivedFps;
float renderedFps;
}
@@ -18,16 +18,16 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers2.db";
private static final String COMPUTER_DB_NAME = "computers3.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
private static final String ADDRESSES_COLUMN_NAME = "Addresses";
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
private static final char ADDRESS_DELIMITER = ';';
private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) {
@@ -47,40 +47,39 @@ public class ComputerDatabaseManager {
}
private void initializeDb(Context c) {
// Add cert column to the table if not present
try {
computerDb.execSQL(String.format((Locale)null,
"ALTER TABLE %s ADD COLUMN %s TEXT",
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
} catch (SQLiteException e) {}
// Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null,
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
COMPUTER_TABLE_NAME,
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT, %s TEXT)",
COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
// Move all computers from the old DB (if any) to the new one
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
for (ComputerDetails computer : oldComputers) {
updateComputer(computer);
}
oldComputers = LegacyDatabaseReader2.migrateAllComputers(c);
for (ComputerDetails computer : oldComputers) {
updateComputer(computer);
}
}
public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
public void deleteComputer(ComputerDetails details) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{details.uuid});
}
public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues();
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
StringBuilder addresses = new StringBuilder();
addresses.append(details.localAddress != null ? details.localAddress : "");
addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? details.remoteAddress : "");
addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? details.manualAddress : "");
addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? details.ipv6Address : "");
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
try {
if (details.serverCert != null) {
@@ -96,18 +95,31 @@ public class ComputerDatabaseManager {
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
private static String readNonEmptyString(String input) {
if (input.isEmpty()) {
return null;
}
return input;
}
private ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails();
details.uuid = c.getString(0);
details.name = c.getString(1);
details.localAddress = c.getString(2);
details.remoteAddress = c.getString(3);
details.manualAddress = c.getString(4);
details.macAddress = c.getString(5);
String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1);
details.localAddress = readNonEmptyString(addresses[0]);
details.remoteAddress = readNonEmptyString(addresses[1]);
details.manualAddress = readNonEmptyString(addresses[2]);
details.ipv6Address = readNonEmptyString(addresses[3]);
details.macAddress = c.getString(3);
try {
byte[] derCertData = c.getBlob(6);
byte[] derCertData = c.getBlob(4);
if (derCertData != null) {
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
@@ -127,14 +139,7 @@ public class ComputerDatabaseManager {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = getComputerFromCursor(c);
// If a critical field is corrupt or missing, skip the database entry
if (details.uuid == null) {
continue;
}
computerList.add(details);
computerList.add(getComputerFromCursor(c));
}
c.close();
@@ -153,12 +158,6 @@ public class ComputerDatabaseManager {
ComputerDetails details = getComputerFromCursor(c);
c.close();
// If a critical field is corrupt or missing, delete the database entry
if (details.uuid == null) {
deleteComputer(details.name);
return null;
}
return details;
}
}
@@ -3,6 +3,7 @@ package com.limelight.computers;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.HashSet;
@@ -220,12 +221,12 @@ public class ComputerManagerService extends Service {
}
}
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
}
public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name);
public void removeComputer(ComputerDetails computer) {
ComputerManagerService.this.removeComputer(computer);
}
public void stopPolling() {
@@ -297,8 +298,27 @@ public class ComputerManagerService extends Service {
return new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
ComputerDetails details = new ComputerDetails();
// Populate the computer template with mDNS info
if (computer.getLocalAddress() != null) {
details.localAddress = computer.getLocalAddress().getHostAddress();
// Since we're on the same network, we can use STUN to find
// 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);
}
}
if (computer.getIpv6Address() != null) {
details.ipv6Address = computer.getIpv6Address().getHostAddress();
}
// Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress().getHostAddress(), false);
if (!addComputerBlocking(details)) {
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
}
}
@Override
@@ -345,24 +365,7 @@ public class ComputerManagerService extends Service {
}
}
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
if (manuallyAdded) {
// Add PC UI
fakeDetails.manualAddress = addr;
}
else {
// mDNS
fakeDetails.localAddress = addr;
// Since we're on the same network, we can use STUN to find
// our WAN address, which is also very likely the WAN address
// of the PC. We can use this later to connect remotely.
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
}
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
// Block while we try to fill the details
try {
// We cannot use runPoll() here because it will attempt to persist the state of the machine
@@ -395,26 +398,22 @@ public class ComputerManagerService extends Service {
return true;
}
else {
if (!manuallyAdded) {
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
}
return false;
}
}
public void removeComputer(String name) {
public void removeComputer(ComputerDetails computer) {
if (!getLocalDatabaseReference()) {
return;
}
// Remove it from the database
dbManager.deleteComputer(name);
dbManager.deleteComputer(computer);
synchronized (pollingTuples) {
// Remove the computer from the computer list
for (PollingTuple tuple : pollingTuples) {
if (tuple.computer.name.equals(name)) {
if (tuple.computer.uuid.equals(computer.uuid)) {
if (tuple.thread != null) {
// Interrupt the thread on this entry
tuple.thread.interrupt();
@@ -514,14 +513,16 @@ public class ComputerManagerService extends Service {
t.start();
}
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException {
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress, final String ipv6Address) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2];
final boolean[] manualInfo = new boolean[2];
final boolean[] ipv6Info = new boolean[2];
startFastPollThread(localAddress, localInfo);
startFastPollThread(remoteAddress, remoteInfo);
startFastPollThread(manualAddress, manualInfo);
startFastPollThread(ipv6Address, ipv6Info);
// Check local first
synchronized (localInfo) {
@@ -545,7 +546,7 @@ public class ComputerManagerService extends Service {
}
}
// And finally, remote
// Now remote IPv4
synchronized (remoteInfo) {
while (!remoteInfo[0]) {
remoteInfo.wait(500);
@@ -556,6 +557,17 @@ public class ComputerManagerService extends Service {
}
}
// Now global IPv6
synchronized (ipv6Info) {
while (!ipv6Info[0]) {
ipv6Info.wait(500);
}
if (ipv6Info[1]) {
return ipv6Address;
}
}
return null;
}
@@ -566,8 +578,8 @@ public class ComputerManagerService extends Service {
// Do not write this address to details.activeAddress because:
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")");
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address);
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
// If no connection could be established to either IP address, there's nothing we can do
@@ -582,8 +594,9 @@ public class ComputerManagerService extends Service {
// already tried
HashSet<String> uniqueAddresses = new HashSet<>();
uniqueAddresses.add(details.localAddress);
uniqueAddresses.add(details.remoteAddress);
uniqueAddresses.add(details.manualAddress);
uniqueAddresses.add(details.remoteAddress);
uniqueAddresses.add(details.ipv6Address);
for (String addr : uniqueAddresses) {
if (addr == null || addr.equals(candidateAddress)) {
continue;
@@ -0,0 +1,86 @@
package com.limelight.computers;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.limelight.nvstream.http.ComputerDetails;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;
public class LegacyDatabaseReader2 {
private static final String COMPUTER_DB_NAME = "computers2.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails();
details.uuid = c.getString(0);
details.name = c.getString(1);
details.localAddress = c.getString(2);
details.remoteAddress = c.getString(3);
details.manualAddress = c.getString(4);
details.macAddress = c.getString(5);
// This column wasn't always present in the old schema
if (c.getColumnCount() >= 7) {
try {
byte[] derCertData = c.getBlob(6);
if (derCertData != null) {
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(derCertData));
}
} catch (CertificateException e) {
e.printStackTrace();
}
}
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
return details;
}
public static List<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = getComputerFromCursor(c);
// If a critical field is corrupt or missing, skip the database entry
if (details.uuid == null) {
continue;
}
computerList.add(details);
}
c.close();
return computerList;
}
public static List<ComputerDetails> migrateAllComputers(Context c) {
SQLiteDatabase computerDb = null;
try {
// Open the existing database
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
return getAllComputers(computerDb);
} catch (SQLiteException e) {
return new LinkedList<ComputerDetails>();
} finally {
// Close and delete the old DB
if (computerDb != null) {
computerDb.close();
}
c.deleteDatabase(COMPUTER_DB_NAME);
}
}
}
@@ -65,7 +65,7 @@ public class DiskAssetLoader {
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
// Don't bother with anything if it doesn't exist
if (!file.exists()) {
@@ -133,6 +133,20 @@ public class DiskAssetLoader {
return bmp;
}
public File getFile(String computerUuid, int appId) {
return CacheHelper.openPath(false, cacheDir, "boxart", computerUuid, appId + ".png");
}
public void deleteAssetsForComputer(String computerUuid) {
File dir = CacheHelper.openPath(false, cacheDir, "boxart", computerUuid);
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
f.delete();
}
}
}
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
OutputStream out = null;
boolean success = false;
@@ -11,6 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
@@ -98,7 +99,9 @@ public class AddComputerManually extends Activity {
getResources().getString(R.string.msg_add_pc), false);
try {
success = managerBinder.addComputerBlocking(host, true);
ComputerDetails details = new ComputerDetails();
details.manualAddress = host;
success = managerBinder.addComputerBlocking(details);
} catch (IllegalArgumentException e) {
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
@@ -31,10 +31,13 @@ public class PreferenceConfiguration {
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
private static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay";
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
static final String DEFAULT_RESOLUTION = "720p";
static final String DEFAULT_FPS = "60";
@@ -54,10 +57,13 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
private static final boolean DEFAULT_ENABLE_HDR = false;
private static final boolean DEFAULT_ENABLE_PIP = false;
private static final boolean DEFAULT_ENABLE_PERF_OVERLAY = false;
private static final boolean DEFAULT_BIND_ALL_USB = false;
private static final boolean DEFAULT_MOUSE_EMULATION = true;
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
private static final boolean DEFAULT_UNLOCK_FPS = false;
private static final boolean DEFAULT_VIBRATE_OSC = true;
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
@@ -75,10 +81,13 @@ public class PreferenceConfiguration {
public boolean disableFrameDrop;
public boolean enableHdr;
public boolean enablePip;
public boolean enablePerfOverlay;
public boolean bindAllUsb;
public boolean mouseEmulation;
public boolean mouseNavButtons;
public boolean unlockFps;
public boolean vibrateOsc;
public boolean vibrateFallbackToDevice;
private static int getHeightFromResolutionString(String resString) {
if (resString.equalsIgnoreCase("360p")) {
@@ -325,10 +334,13 @@ public class PreferenceConfiguration {
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
config.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY);
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
return config;
}
@@ -4,8 +4,10 @@ import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import com.limelight.AppView;
import com.limelight.Game;
import com.limelight.R;
import com.limelight.ShortcutTrampoline;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
@@ -25,6 +27,25 @@ public class ServerHelper {
return computer.activeAddress;
}
public static Intent createPcShortcutIntent(Activity parent, ComputerDetails computer) {
Intent i = new Intent(parent, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
i.setAction(Intent.ACTION_DEFAULT);
return i;
}
public static Intent createAppShortcutIntent(Activity parent, ComputerDetails computer, NvApp app) {
Intent i = new Intent(parent, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
i.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
i.putExtra(Game.EXTRA_APP_ID, ""+app.getAppId());
i.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
i.setAction(Intent.ACTION_DEFAULT);
return i;
}
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
@@ -1,7 +1,7 @@
package com.limelight.utils;
import android.annotation.TargetApi;
import android.content.Context;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
@@ -22,9 +22,10 @@ import java.util.List;
public class ShortcutHelper {
private final ShortcutManager sm;
private final Context context;
private final Activity context;
private final TvChannelHelper tvChannelHelper;
public ShortcutHelper(Context context) {
public ShortcutHelper(Activity context) {
this.context = context;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
sm = context.getSystemService(ShortcutManager.class);
@@ -32,6 +33,7 @@ public class ShortcutHelper {
else {
sm = null;
}
this.tvChannelHelper = new TvChannelHelper(context);
}
@TargetApi(Build.VERSION_CODES.N_MR1)
@@ -80,39 +82,39 @@ public class ShortcutHelper {
return false;
}
public void reportShortcutUsed(String id) {
public void reportComputerShortcutUsed(ComputerDetails computer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
if (getInfoForId(id) != null) {
sm.reportShortcutUsed(id);
if (getInfoForId(computer.uuid) != null) {
sm.reportShortcutUsed(computer.uuid);
}
}
}
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
Intent i = new Intent(context, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.setAction(Intent.ACTION_DEFAULT);
public void reportGameLaunched(ComputerDetails computer, NvApp app) {
tvChannelHelper.createTvChannel(computer);
tvChannelHelper.addGameToChannel(computer, app);
}
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
.setIntent(i)
.setShortLabel(computerName)
.setLongLabel(computerName)
public void createAppViewShortcut(ComputerDetails computer, boolean forceAdd, boolean newlyPaired) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, computer.uuid)
.setIntent(ServerHelper.createPcShortcutIntent(context, computer))
.setShortLabel(computer.name)
.setLongLabel(computer.name)
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
.build();
ShortcutInfo existingSinfo = getInfoForId(id);
ShortcutInfo existingSinfo = getInfoForId(computer.uuid);
if (existingSinfo != null) {
// Update in place
sm.updateShortcuts(Collections.singletonList(sinfo));
sm.enableShortcuts(Collections.singletonList(id));
sm.enableShortcuts(Collections.singletonList(computer.uuid));
}
// Reap shortcuts to make space for this if it's new
// NOTE: This CAN'T be an else on the above if, because it's
// possible that we have an existing shortcut but it's not a dynamic one.
if (!isExistingDynamicShortcut(id)) {
if (!isExistingDynamicShortcut(computer.uuid)) {
// To avoid a random carousel of shortcuts popping in and out based on polling status,
// we only add shortcuts if it's not at the limit or the user made a conscious action
// to interact with this PC.
@@ -122,22 +124,26 @@ public class ShortcutHelper {
}
}
}
if (newlyPaired) {
// Avoid hammering the channel API for each computer poll because it will throttle us
tvChannelHelper.createTvChannel(computer);
tvChannelHelper.requestChannelOnHomeScreen(computer);
}
}
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
createAppViewShortcut(id, details.name, details.uuid, forceAdd);
public void createAppViewShortcutForOnlineHost(ComputerDetails details) {
createAppViewShortcut(details, false, false);
}
private String getShortcutIdForGame(ComputerDetails computer, NvApp app) {
return computer.uuid + app.getAppId();
}
@TargetApi(Build.VERSION_CODES.O)
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
public boolean createPinnedGameShortcut(ComputerDetails computer, NvApp app, Bitmap iconBits) {
if (sm.isRequestPinShortcutSupported()) {
Icon appIcon;
Intent i = new Intent(context, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
i.setAction(Intent.ACTION_DEFAULT);
if (iconBits != null) {
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
@@ -145,9 +151,9 @@ public class ShortcutHelper {
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
}
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
.setIntent(i)
.setShortLabel(appName + " (" + computerName + ")")
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, getShortcutIdForGame(computer, app))
.setIntent(ServerHelper.createAppShortcutIntent(context, computer, app))
.setShortLabel(app.getAppName() + " (" + computer.name + ")")
.setIcon(appIcon)
.build();
@@ -157,12 +163,30 @@ public class ShortcutHelper {
}
}
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid, app.getAppName(), Integer.valueOf(app.getAppId()).toString());
public void disableComputerShortcut(ComputerDetails computer, CharSequence reason) {
tvChannelHelper.deleteChannel(computer);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
// Delete the computer shortcut itself
if (getInfoForId(computer.uuid) != null) {
sm.disableShortcuts(Collections.singletonList(computer.uuid), reason);
}
// Delete all associated app shortcuts too
List<ShortcutInfo> shortcuts = getAllShortcuts();
LinkedList<String> appShortcutIds = new LinkedList<>();
for (ShortcutInfo info : shortcuts) {
if (info.getId().startsWith(computer.uuid)) {
appShortcutIds.add(info.getId());
}
}
sm.disableShortcuts(appShortcutIds, reason);
}
}
public void disableShortcut(String id, CharSequence reason) {
public void disableAppShortcut(ComputerDetails computer, NvApp app, CharSequence reason) {
tvChannelHelper.deleteProgram(computer, app);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
String id = getShortcutIdForGame(computer, app);
if (getInfoForId(id) != null) {
sm.disableShortcuts(Collections.singletonList(id), reason);
}
@@ -0,0 +1,345 @@
package com.limelight.utils;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.media.tv.TvContract;
import android.net.Uri;
import android.os.Build;
import com.limelight.LimeLog;
import com.limelight.PosterContentProvider;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.OutputStream;
public class TvChannelHelper {
private static final int ASPECT_RATIO_MOVIE_POSTER = 5;
private static final int TYPE_GAME = 12;
private static final int INTERNAL_PROVIDER_ID_INDEX = 1;
private static final int PROGRAM_BROWSABLE_INDEX = 2;
private static final int ID_INDEX = 0;
private Activity context;
public TvChannelHelper(Activity context) {
this.context = context;
}
void requestChannelOnHomeScreen(ComputerDetails computer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
Long channelId = getChannelId(computer.uuid);
if (channelId == null) {
return;
}
Intent intent = new Intent(TvContract.ACTION_REQUEST_CHANNEL_BROWSABLE);
intent.putExtra(TvContract.EXTRA_CHANNEL_ID, getChannelId(computer.uuid));
try {
context.startActivityForResult(intent, 0);
} catch (ActivityNotFoundException e) {
}
}
}
void createTvChannel(ComputerDetails computer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
ChannelBuilder builder = new ChannelBuilder()
.setType(TvContract.Channels.TYPE_PREVIEW)
.setDisplayName(computer.name)
.setInternalProviderId(computer.uuid)
.setAppLinkIntent(ServerHelper.createPcShortcutIntent(context, computer));
Long channelId = getChannelId(computer.uuid);
if (channelId != null) {
context.getContentResolver().update(TvContract.buildChannelUri(channelId),
builder.toContentValues(), null, null);
return;
}
Uri channelUri = context.getContentResolver().insert(
TvContract.Channels.CONTENT_URI, builder.toContentValues());
if (channelUri != null) {
long id = ContentUris.parseId(channelUri);
updateChannelIcon(id);
}
}
}
@TargetApi(Build.VERSION_CODES.O)
private void updateChannelIcon(long channelId) {
Bitmap logo = drawableToBitmap(context.getResources().getDrawable(R.drawable.ic_channel));
try {
Uri localUri = TvContract.buildChannelLogoUri(channelId);
try (OutputStream outputStream = context.getContentResolver().openOutputStream(localUri)) {
logo.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
outputStream.flush();
} catch (SQLiteException | IOException e) {
LimeLog.warning("Failed to store the logo to the system content provider.");
e.printStackTrace();
}
} finally {
logo.recycle();
}
}
private Bitmap drawableToBitmap(Drawable drawable) {
int width = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
int height = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
void addGameToChannel(ComputerDetails computer, NvApp app) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
Long channelId = getChannelId(computer.uuid);
if (channelId == null) {
return;
}
PreviewProgramBuilder builder = new PreviewProgramBuilder()
.setChannelId(channelId)
.setType(TYPE_GAME)
.setTitle(app.getAppName())
.setPosterArtAspectRatio(ASPECT_RATIO_MOVIE_POSTER)
.setPosterArtUri(PosterContentProvider.createBoxArtUri(computer.uuid, ""+app.getAppId()))
.setIntent(ServerHelper.createAppShortcutIntent(context, computer, app))
.setInternalProviderId(""+app.getAppId())
// Weight should increase each time we run the game
.setWeight((int)((System.currentTimeMillis() - 1500000000000L) / 1000));
Long programId = getProgramId(channelId, ""+app.getAppId());
if (programId != null) {
context.getContentResolver().update(TvContract.buildPreviewProgramUri(programId),
builder.toContentValues(), null, null);
return;
}
context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
builder.toContentValues());
TvContract.requestChannelBrowsable(context, channelId);
}
}
void deleteChannel(ComputerDetails computer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
Long channelId = getChannelId(computer.uuid);
if (channelId == null) {
return;
}
context.getContentResolver().delete(TvContract.buildChannelUri(channelId), null, null);
}
}
void deleteProgram(ComputerDetails computer, NvApp app) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
Long channelId = getChannelId(computer.uuid);
if (channelId == null) {
return;
}
Long programId = getProgramId(channelId, ""+app.getAppId());
if (programId == null) {
return;
}
context.getContentResolver().delete(TvContract.buildPreviewProgramUri(programId), null, null);
}
}
@TargetApi(Build.VERSION_CODES.O)
private Long getChannelId(String computerUuid) {
try (Cursor cursor = context.getContentResolver().query(
TvContract.Channels.CONTENT_URI,
new String[] {TvContract.Channels._ID, TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID},
null,
null,
null)) {
if (cursor == null || cursor.getCount() == 0) {
return null;
}
while (cursor.moveToNext()) {
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
if (computerUuid.equals(internalProviderId)) {
return cursor.getLong(ID_INDEX);
}
}
return null;
}
}
@TargetApi(Build.VERSION_CODES.O)
private Long getProgramId(long channelId, String appId) {
try (Cursor cursor = context.getContentResolver().query(
TvContract.buildPreviewProgramsUriForChannel(channelId),
new String[] {TvContract.PreviewPrograms._ID, TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, TvContract.PreviewPrograms.COLUMN_BROWSABLE},
null,
null,
null)) {
if (cursor == null || cursor.getCount() == 0) {
return null;
}
while (cursor.moveToNext()) {
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
if (appId.equals(internalProviderId)) {
long id = cursor.getLong(ID_INDEX);
int browsable = cursor.getInt(PROGRAM_BROWSABLE_INDEX);
if (browsable != 0) {
return id;
} else {
int countDeleted = context.getContentResolver().delete(TvContract.buildPreviewProgramUri(id), null, null);
if (countDeleted > 0) {
LimeLog.info("Preview program has been deleted");
} else {
LimeLog.warning("Preview program has not been deleted");
}
}
}
}
return null;
}
}
private static <T> String toValueString(T value) {
return value == null ? null : value.toString();
}
private static String toUriString(Intent intent) {
return intent == null ? null : intent.toUri(Intent.URI_INTENT_SCHEME);
}
@TargetApi(Build.VERSION_CODES.O)
private boolean isAndroidTV() {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
@TargetApi(Build.VERSION_CODES.O)
private static class PreviewProgramBuilder {
private ContentValues mValues = new ContentValues();
public PreviewProgramBuilder setChannelId(Long channelId) {
mValues.put(TvContract.PreviewPrograms.COLUMN_CHANNEL_ID, channelId);
return this;
}
public PreviewProgramBuilder setType(int type) {
mValues.put(TvContract.PreviewPrograms.COLUMN_TYPE, type);
return this;
}
public PreviewProgramBuilder setTitle(String title) {
mValues.put(TvContract.PreviewPrograms.COLUMN_TITLE, title);
return this;
}
public PreviewProgramBuilder setPosterArtAspectRatio(int aspectRatio) {
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, aspectRatio);
return this;
}
public PreviewProgramBuilder setIntent(Intent intent) {
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toUriString(intent));
return this;
}
public PreviewProgramBuilder setIntentUri(Uri uri) {
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toValueString(uri));
return this;
}
public PreviewProgramBuilder setInternalProviderId(String id) {
mValues.put(TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, id);
return this;
}
public PreviewProgramBuilder setPosterArtUri(Uri uri) {
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_URI, toValueString(uri));
return this;
}
public PreviewProgramBuilder setWeight(int weight) {
mValues.put(TvContract.PreviewPrograms.COLUMN_WEIGHT, weight);
return this;
}
public ContentValues toContentValues() {
return new ContentValues(mValues);
}
}
@TargetApi(Build.VERSION_CODES.O)
private static class ChannelBuilder {
private ContentValues mValues = new ContentValues();
public ChannelBuilder setType(String type) {
mValues.put(TvContract.Channels.COLUMN_TYPE, type);
return this;
}
public ChannelBuilder setDisplayName(String displayName) {
mValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, displayName);
return this;
}
public ChannelBuilder setInternalProviderId(String internalProviderId) {
mValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
return this;
}
public ChannelBuilder setAppLinkIntent(Intent intent) {
mValues.put(TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, toUriString(intent));
return this;
}
public ContentValues toContentValues() {
return new ContentValues(mValues);
}
}
}
@@ -2,13 +2,12 @@ package com.limelight.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import com.limelight.R;
@@ -19,10 +18,6 @@ import java.util.Locale;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
private static final int TV_VERTICAL_PADDING_DP = 27;
private static final int TV_HORIZONTAL_PADDING_DP = 48;
public static void setLocale(Activity activity)
{
String locale = PreferenceConfiguration.readPreferences(activity).language;
@@ -47,20 +42,6 @@ public class UiHelper {
public static void notifyNewRootView(Activity activity)
{
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.
//
@@ -70,6 +51,23 @@ public class UiHelper {
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Draw under the status bar on Android Q devices
activity.getWindow().getDecorView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
view.setPadding(windowInsets.getSystemWindowInsetLeft(),
windowInsets.getSystemWindowInsetTop(),
windowInsets.getSystemWindowInsetRight(),
0);
return windowInsets;
}
});
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
}
public static void showDecoderCrashDialog(Activity activity) {

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/ic_launcher_background"/>
<item android:drawable="@drawable/ic_lime_layer"
android:bottom="10dp"
android:left="10dp"
android:right="10dp"
android:top="10dp"
/>
</layer-list>
@@ -0,0 +1,19 @@
<vector android:height="24dp" android:viewportHeight="546.15576"
android:viewportWidth="546.08374" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="M252.584,0.356c-58.2,4.8 -112.3,27.1 -156.8,64.6l-8.4,7 86.9,86.9c47.7,47.7 87.1,86.8 87.6,86.8 0.4,-0 0.6,-55.2 0.5,-122.8l-0.3,-122.7 -2.5,-0.1c-1.4,-0 -4.5,0.1 -7,0.3z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M284.284,0.356c-1,0.9 -0.9,245.3 0.1,245.3 0.4,-0 39.8,-39.1 87.5,-86.8l86.9,-86.9 -8.4,-7c-34.4,-29 -74.9,-49.2 -117.8,-58.7 -16.6,-3.6 -46.9,-7.4 -48.3,-5.9z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M64.884,95.856c-24.1,28.5 -41.2,59.5 -52.6,95.3 -6.3,19.6 -11.1,45.8 -11.9,64l-0.3,7 122.8,0.3c67.5,0.1 122.7,-0.1 122.7,-0.5 0,-0.5 -39.1,-39.9 -86.8,-87.6l-86.9,-86.9 -7,8.4z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M387.384,174.356c-47.8,47.7 -86.8,87.1 -86.8,87.6 0,0.4 55.2,0.6 122.8,0.5l122.7,-0.3 -0.3,-7c-1.4,-32.6 -12.4,-72.9 -28.7,-105.5 -9.1,-18.2 -25.9,-43 -38.7,-57.3l-4.3,-4.8 -86.7,86.8z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M0.184,284.556c-0.7,1.1 1,19.8 3,32.1 7.6,48.6 29.1,95.2 61.7,133.8l7,8.4 86.9,-86.9c47.7,-47.7 86.8,-87.1 86.8,-87.5 0,-1.1 -244.8,-1 -245.4,0.1z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M300.584,284.356c0,0.5 39.1,39.9 86.8,87.6l86.9,86.9 7,-8.4c24.1,-28.5 41.2,-59.5 52.6,-95.3 6.3,-19.6 11.1,-45.8 11.9,-64l0.3,-7 -122.7,-0.3c-67.6,-0.1 -122.8,0.1 -122.8,0.5z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M174.284,387.456l-86.9,86.9 8.4,7c28.5,24.1 59.5,41.2 95.3,52.6 19.6,6.3 45.8,11.1 64,11.9l7,0.3 0.3,-122.8c0.1,-67.5 -0.1,-122.7 -0.5,-122.7 -0.5,-0 -39.9,39.1 -87.6,86.8z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M283.784,423.356l0.3,122.8 7,-0.3c18.2,-0.8 44.4,-5.6 64,-11.9 35.8,-11.4 66.8,-28.5 95.3,-52.6l8.4,-7 -86.9,-86.9c-47.7,-47.7 -87.1,-86.8 -87.6,-86.8 -0.4,-0 -0.6,55.2 -0.5,122.7z" android:strokeColor="#00000000"/>
</vector>
+8 -4
View File
@@ -1,5 +1,9 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"
android:fillColor="#FFFFFF"/>
</vector>
+4 -8
View File
@@ -1,9 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4 -1.5,0 -2.89,0.19 -4.15,0.48L18.18,13.8 23.64,7zM17.04,15.22L3.27,1.44 2,2.72l2.05,2.06C1.91,5.76 0.59,6.82 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01 3.9,-4.86 3.32,3.32 1.27,-1.27 -3.46,-3.46z"
android:fillColor="#FFFFFF"/>
<vector android:height="128dp" android:width="128dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>
@@ -2,7 +2,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
@@ -2,10 +2,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" >
<FrameLayout
@@ -27,8 +23,8 @@
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:gravity="center"
android:paddingTop="0dp"
android:paddingBottom="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textSize="28sp"/>
</RelativeLayout>
+26
View File
@@ -10,4 +10,30 @@
android:layout_height="match_parent"
android:layout_gravity="center" />
<TextView
android:id="@+id/performanceOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_gravity="left"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="left"
android:background="#80000000"
android:visibility="gone" />
<TextView
android:id="@+id/notificationOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
android:layout_gravity="right"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="right"
android:background="#80000000"
android:visibility="gone" />
</merge>
@@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
@@ -2,10 +2,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:id="@+id/stream_settings"
tools:context=".preferences.StreamSettings">
+10 -14
View File
@@ -1,15 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
+10 -14
View File
@@ -1,15 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
+11 -14
View File
@@ -1,15 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:gravity="center"/>
</LinearLayout>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:gravity="center"/>
+11 -14
View File
@@ -1,15 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:gravity="center"/>
</LinearLayout>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:gravity="center"/>
+12 -1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Shortcut strings -->
<string name="scut_deleted_pc">PC supprimé</string>
<string name="scut_not_paired">PC non appairé</string>
@@ -81,6 +82,9 @@
<string name="title_details">Détails</string>
<string name="help">Aide</string>
<string name="delete_pc_msg">Êtes-vous sûr de vouloir supprimer ce PC?</string>
<string name="slow_connection_msg">Connexion lente au PC\nRéduisez votre débit</string>
<string name="poor_connection_msg">Mauvaise connexion au PC</string>
<string name="perf_overlay_text">Video dimensions: %1$s\nDécodeur: %2$s\nEstimation de la fréquence d\'images de l\'ordinateur hôte: %3$.2f FPS\nFréquence d\'images entrantes du réseau: %4$.2f FPS\nTaux de rendu: %5$.2f FPS\nImages envoyé par votre connexion réseau: %6$.2f%%\nTemps moyen de réception: %7$.2f ms\nTemps de décodage moyen: %8$.2f ms</string>
<!-- AppList activity -->
<string name="applist_connect_msg">Connexion au PC…</string>
@@ -90,6 +94,7 @@
<string name="applist_menu_cancel">Annuler</string>
<string name="applist_menu_details">Voir les détails</string>
<string name="applist_menu_scut">Créer un raccourci</string>
<string name="applist_menu_tv_channel">Ajouter à la chaîne</string>
<string name="applist_refresh_title">Liste des applications</string>
<string name="applist_refresh_msg">Actualisation des applications…</string>
<string name="applist_refresh_error_title">Erreur</string>
@@ -133,6 +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_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>
@@ -147,6 +154,8 @@
<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="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>
@@ -176,5 +185,7 @@
<string name="summary_video_format">H.265 réduit les besoins en bande passante vidéo mais nécessite un périphérique très récent</string>
<string name="title_enable_hdr">Activer le HDR (expérimental)</string>
<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>
</resources>
+27
View File
@@ -66,6 +66,7 @@
<string name="applist_menu_quit">Выйти из сессии</string>
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
<string name="applist_menu_cancel">Отмена</string>
<string name="applist_menu_tv_channel">Добавать на канал</string>
<string name="applist_refresh_title">Список приложений</string>
<string name="applist_refresh_msg">Обновление приложений…</string>
<string name="applist_refresh_error_title">Ошибка</string>
@@ -156,4 +157,30 @@
<string name="title_enable_hdr">Включить HDR (Экспериментально)</string>
<string name="summary_enable_hdr">Транслировать в HDR если игра и GPU компьютера поддерживают это. HDR требует видеокарты GTX 1000 серии или более новой.</string>
<string name="title_checkbox_vibrate_osc">Включить вибрацию</string>
<string name="title_fps_list">Частота кадров</string>
<string name="applist_menu_details">Детали</string>
<string name="applist_menu_scut">Создать ярлык</string>
<string name="category_input_settings">Настройки ввода</string>
<string name="delete_pc_msg">Вы уверены что хотите удалить этот PC?</string>
<string name="pcview_menu_details">Детали</string>
<string name="poor_connection_msg">Слабое соединение с PC</string>
<string name="title_details">Детали</string>
<string name="title_enable_perf_overlay">Включить отображение статистики</string>
<string name="title_unlock_fps">Разблокировать все возможные частоты обновления</string>
<string name="applist_details_id">ID приложения:</string>
<string name="title_checkbox_vibrate_fallback">Эмуляция виброотдачи</string>
<string name="summary_checkbox_vibrate_osc">Вибрировать устройство для эмуляции виброотдачи при экранном управлении</string>
<string name="summary_checkbox_vibrate_fallback">Вибрировать устройство для эмуляции виброотдачи для геймпадов без поддержки вибрации</string>
<string name="summary_checkbox_mouse_nav_buttons">Включение этой опции может привести к неправильной работе правой кнопки мыши на некоторых устройствах</string>
<string name="scut_pc_not_found">PC не найден</string>
<string name="unable_to_pin_shortcut">Текущий лаунчер не позволяет создавать pinned ярлыки</string>
<string name="title_checkbox_mouse_nav_buttons">Включить кнопки вперед и назад для мыши</string>
<string name="slow_connection_msg">Медленное подключение к PC\nУменьшите битрейт</string>
<string name="summary_unlock_fps">Трансляция со скоростью 90 или 120 кадров в секунду может уменьшить задержку на устройствах высокого класса, но может вызвать задержки или сбой на устройствах без поддержки этого функционала</string>
<string name="summary_enable_perf_overlay">Отображение оверлея на экране с информацией о производительности во время трансляции в режиме реального времени</string>
<string name="perf_overlay_text">Разрешение видео: %1$s\nДекодер: %2$s\nРасчетная частота кадров PC-хоста: %3$.2f FPS\nВходящая частота кадров из сети: %4$.2f FPS\nЧастота кадров при рендеринге: %5$.2f FPS\nОтброшеных кадров вашей сетью: %6$.2f%%\nСреднее время получения: %7$.2f ms\nСреднее время декодирования: %8$.2f ms</string>
<string name="summary_fps_list">Увеличение для более плавного видео потока. Уменьшите для лучшей производительности на более слабых устройствах.</string>
<string name="scut_invalid_uuid">Указанный PC недействителен</string>
<string name="scut_invalid_app_id">Указанное приложение недействительно</string>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<resources>
<style name="AppBaseTheme" parent="android:Theme.Material">
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
@@ -136,5 +136,28 @@
<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>
</resources>
+32 -5
View File
@@ -97,27 +97,51 @@
<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="title_fps_list">影像幀數</string>
<string name="summary_fps_list">增加以提供更流暢的影像串流. 減少以在較低的配備上獲得較好的效能.</string>
<string name="title_seekbar_bitrate"> 選擇目標影像碼率 </string>
<string name="summary_seekbar_bitrate"> 低碼率減少卡頓,高碼率提高畫質 </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="title_checkbox_enable_pip">啟用子母畫面</string>
<string name="summary_checkbox_enable_pip">當多工處理時, 允許觀看串流畫面(無法控制)</string>
<string name="category_audio_settings"> 音頻設置 </string>
<string name="title_checkbox_51_surround"> 啟用 5.1 環繞音效 </string>
<string name="summary_checkbox_51_surround"> 如果你的聲音聽起來有問題請禁用。\n\n需要GeForce Experience 2.7 或更高版本 </string>
<string name="category_input_settings">輸入設定</string>
<string name="title_checkbox_multi_controller"> 啟用多手柄支持 </string>
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄將會認作一個手柄 </string>
<string name="title_seekbar_deadzone"> 調整搖桿死區 </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="category_on_screen_controls_settings">設置 </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>
@@ -134,7 +158,10 @@
<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="summary_video_format">H.265能降低寬需求,但是需要設備支持 </string>
<string name="title_enable_hdr">啟用 HDR (實驗中)</string>
<string name="summary_enable_hdr">啟用 HDR 當遊戲與電腦的顯示卡支援時. HDR 需要 GTX 1000 系列之顯示卡或更高.</string>
</resources>
+2
View File
@@ -3,5 +3,7 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="tv_channel_logo_width">80dp</dimen>
<dimen name="tv_channel_logo_height">80dp</dimen>
</resources>
+10
View File
@@ -84,6 +84,9 @@
<string name="title_details">Details</string>
<string name="help">Help</string>
<string name="delete_pc_msg">Are you sure you want to delete this PC?</string>
<string name="slow_connection_msg">Slow connection to PC\nReduce your bitrate</string>
<string name="poor_connection_msg">Poor connection to PC</string>
<string name="perf_overlay_text">Video dimensions: %1$s\nDecoder: %2$s\nEstimated host PC frame rate: %3$.2f FPS\nIncoming frame rate from network: %4$.2f FPS\nRendering frame rate: %5$.2f FPS\nFrames dropped by your network connection: %6$.2f%%\nAverage receive time: %7$.2f ms\nAverage decoding time: %8$.2f ms</string>
<!-- AppList activity -->
<string name="applist_connect_msg">Connecting to PC…</string>
@@ -93,6 +96,7 @@
<string name="applist_menu_cancel">Cancel</string>
<string name="applist_menu_details">View Details</string>
<string name="applist_menu_scut">Create Shortcut</string>
<string name="applist_menu_tv_channel">Add to Channel</string>
<string name="applist_refresh_title">App List</string>
<string name="applist_refresh_msg">Refreshing apps…</string>
<string name="applist_refresh_error_title">Error</string>
@@ -136,6 +140,8 @@
<string name="category_input_settings">Input Settings</string>
<string name="title_checkbox_multi_controller">Automatic gamepad presence detection</string>
<string name="summary_checkbox_multi_controller">Unchecking this option forces a gamepad to always be present</string>
<string name="title_checkbox_vibrate_fallback">Emulate rumble support with vibration</string>
<string name="summary_checkbox_vibrate_fallback">Vibrates your device to emulate rumble if your gamepad does not support it</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string>
@@ -150,6 +156,8 @@
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
<string name="summary_checkbox_show_onscreen_controls">Show virtual controller overlay on touchscreen</string>
<string name="title_checkbox_vibrate_osc">Enable vibration</string>
<string name="summary_checkbox_vibrate_osc">Vibrates your device to emulate rumble for the on-screen controls</string>
<string name="title_only_l3r3">Only show L3 and R3</string>
<string name="summary_only_l3r3">Hide all virtual buttons except L3 and R3</string>
<string name="title_reset_osc">Clear saved on-screen controls layout</string>
@@ -179,5 +187,7 @@
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device</string>
<string name="title_enable_hdr">Enable HDR (Experimental)</string>
<string name="summary_enable_hdr">Stream HDR when the game and PC GPU support it. HDR requires a GTX 1000 series GPU or later.</string>
<string name="title_enable_perf_overlay">Enable performance overlay</string>
<string name="summary_enable_perf_overlay">Display an on-screen overlay with real-time performance information while streaming</string>
</resources>
+21
View File
@@ -82,6 +82,11 @@
android:title="@string/title_checkbox_mouse_emulation"
android:summary="@string/summary_checkbox_mouse_emulation"
android:defaultValue="true" />
<CheckBoxPreference
android:key="checkbox_vibrate_fallback"
android:title="@string/title_checkbox_vibrate_fallback"
android:summary="@string/summary_checkbox_vibrate_fallback"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
android:key="category_onscreen_controls">
@@ -90,6 +95,12 @@
android:key="checkbox_show_onscreen_controls"
android:summary="@string/summary_checkbox_show_onscreen_controls"
android:title="@string/title_checkbox_show_onscreen_controls" />
<CheckBoxPreference
android:key="checkbox_vibrate_osc"
android:dependency="checkbox_show_onscreen_controls"
android:title="@string/title_checkbox_vibrate_osc"
android:summary="@string/summary_checkbox_vibrate_osc"
android:defaultValue="true" />
<CheckBoxPreference
android:defaultValue="false"
android:dependency="checkbox_show_onscreen_controls"
@@ -138,6 +149,11 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"
android:key="category_advanced_settings">
<CheckBoxPreference
android:key="checkbox_disable_warnings"
android:title="@string/title_checkbox_disable_warnings"
android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" />
<ListPreference
android:key="video_format"
android:title="@string/title_video_format"
@@ -155,5 +171,10 @@
android:title="@string/title_enable_hdr"
android:summary="@string/summary_enable_hdr"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_enable_perf_overlay"
android:title="@string/title_enable_perf_overlay"
android:summary="@string/summary_enable_perf_overlay"
android:defaultValue="false"/>
</PreferenceCategory>
</PreferenceScreen>
+1 -1
View File
@@ -5,7 +5,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.android.tools.build:gradle:3.4.2'
}
}
@@ -0,0 +1,34 @@
Diese App streamt Spiele, Programme oder den kompletten Desktop von NVIDIA GameStream-kompatiebelen PCs mit NVIDIA GeForce Experience über das Lokale Netzwerk oder Internet. Gleichzeitig werden Maus-, Tastatur- und Gamepad-Eingaben von deinem Android Gerät zum PC übertragen.
Die Streamingqualität kann aufgrund des verwendeten Android Geräts und der Netzwerkgegebenheiten variieren. HDR Unterstützung setzt ein HRD10-fähiges Gerät, eine GTX-1000-series GPU und HDR10-kompatibles Spiele voraus.
'''Merkmale'''
* Open-source und komplett freie Software (keine Werbung, InApp-Käufe, oder "Pro" Version)
* Streamt Spiele unabhängig davon in welchem Store diese gekauft wurden
* Funktioniert im Heimnetzwerk oder über eine Internet bzw. LTE-Verbindung
* Bis zu 4K HDR Streaming mit 120 FPS und 5.1 Sourround Sound
* Tastatur- und Mausznterstützung (mit Android 8.0 oder mit Root-Rechten)
* Unterstützt PS3, PS4, Xbox 360, Xbox One und Android Gamepads
* Force Feedback Unterstützung
* Kooperatives lokales Spielen mit bis zu 4 verbundenen Eingabegeräten
* Maussteuerung via Gamepad durch langes gedrückt halten der Starttaste
'''PC Anforderungen'''
* NVIDIA GeForce GTX/RTX Serie GPU (''GT-Serie und AMD GPUs werden nicht von NVIDIA GameStream unterstützt'')
* Windows 7 oder neuer
* NVIDIA GeForce Experience (GFE) 2.2.2 oder neuer
'''Anleitung zur Schnellkonfiguration'''
# Stellen sie sicher dass GeForce Experience auf ihrem PC installiert ist. Aktivieren sie GameStream in den SHIELD Einstellungen.
# Wählen Sie den PC in Moonlight aus und tippen sie den PIN auf ihrem PC ein.
# Streamen sie los!
Für eine gutes Benutzungserlebnis benötigen sie einen mid- bist high-end WLAN-Router mit einer ungestörten Verbindung zu ihrem Android Gerät (5 GHz wird empfohlen) sowie eine gute Verbindung von ihrem PC zum Router (Ethernet empfohlen).
'''detaillierte Konfigurationsanleitung'''
Besuchen die die vollständige Konfigurationsanleitung https://bit.ly/1skHFjN (Englisch) um:
* Einen PC manuell hinzuzufügen (falls ihr PC nicht automatisch erkannt werden sollte)
* Über das Internet bzw. LTE zu streamen
* Eine Eingabegerät das direkt mit dem PC verbunden ist zu nutzen
* Den kompletten Desktop zu streamen
* Individuelle Apps zum Streamen hinzuzufügen
@@ -0,0 +1 @@
Spiele vom deinem PC auf Android spielen (nur NVIDIA)
+1
View File
@@ -0,0 +1 @@
Moonlight Spiele Streaming
@@ -0,0 +1,5 @@
- Updated to target Android Q SDK
- Requested low latency WiFi behavior on Android Q
- Fixed mouse capture in multi-window mode on Android Q
- Updated visual styles to match gesture navigation on Android Q
- Fixed USB driver issue that could cause player numbers to be wrong
@@ -0,0 +1,7 @@
- Added a performance overlay for real-time performance data
- Added support for launching games directly from the Android TV homescreen
- Added support for zero-configuration Internet streaming on IPv6 networks
- Improved handling of home screen PC and game shortcuts
- Fixed streaming from very old versions of GeForce Experience
- Fixed deleting PCs with duplicate names
- Updated Simplified Chinese translation
@@ -0,0 +1,8 @@
- Optimized edge-to-edge layout for Android Q
- Fixed 5.1 surround sound not always working over the Internet
- Fixed Android TV app icon on Android Pie
- Improved reliability of public IP address detection
- Enabled installation on external storage
- Fixed on-screen overlays covering stream when in PiP mode
- Fixed games never reappearing on Android TV channel if deleted once
- Updated Russian and French translations
@@ -0,0 +1,34 @@
This app streams games, programs, or your full desktop from an NVIDIA GameStream-compatible PC on your local network or the Internet using NVIDIA GeForce Experience. Mouse, keyboard, and controller input is sent from your Android device to the PC.
Streaming performance may vary based on your client device and network setup. HDR requires an HDR10-capable device, GTX 1000-series GPU, and HDR10-enabled game.
'''Features'''
* Open-source and completely free (no ads, IAPs, or "Pro")
* Streams games purchased from any store
* Works on your home network or over the Internet/LTE
* Up to 4K 120 FPS HDR streaming with 5.1 surround sound
* Keyboard and mouse support (with Android 8.0 or rooted device)
* Supports PS3, PS4, Xbox 360, Xbox One, and Android gamepads
* Force feedback support
* Local co-op with up to 4 connected controllers
* Mouse control via gamepad by long-pressing Start
'''PC Requirements'''
* NVIDIA GeForce GTX/RTX series GPU (''GT-series and AMD GPUs aren't supported by NVIDIA GameStream'')
* Windows 7 or later
* NVIDIA GeForce Experience (GFE) 2.2.2 or later
'''Quick Setup Instructions'''
# Make sure GeForce Experience is open on your PC. Turn on GameStream in the SHIELD settings page.
# Tap on the PC in Moonlight and type the PIN on your PC
# Start streaming!
To have a good experience, you need a mid to high-end wireless router with a good wireless connection to your Android device (5 GHz highly recommended) and a good connection from your PC to your router (Ethernet highly recommended).
'''Detailed Setup Instructions'''
See the full setup guide https://bit.ly/1skHFjN for:
* Adding a PC manually (if your PC is not detected)
* Streaming over the Internet or LTE
* Using a controller connected directly to your PC
* Streaming your full desktop
* Adding custom apps to stream

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

@@ -0,0 +1 @@
Play games from your PC on Android (NVIDIA-only)
@@ -0,0 +1 @@
Moonlight Game Streaming
+2 -2
View File
@@ -1,6 +1,6 @@
#Tue Feb 05 20:54:22 PST 2019
#Fri Apr 26 18:29:34 PDT 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
Vendored Regular → Executable
View File