Compare commits

...

37 Commits

Author SHA1 Message Date
Cameron Gutman b9f1142af7 Version 5.2 2017-09-09 18:53:36 -07:00
Cameron Gutman 38a6a2b74a A few fixes for decoder crash notifications 2017-09-09 18:44:06 -07:00
Cameron Gutman fd2421618a Update common-c with crash fix 2017-09-09 17:40:53 -07:00
Cameron Gutman 79a9ea7179 Add decoder crash notification and settings reset on continued crashing 2017-09-09 17:40:07 -07:00
Cameron Gutman 34a11c9262 Correct reachability when restoring a lost address 2017-09-09 16:02:39 -07:00
Cameron Gutman 84a9845c1d Fix polling overwriting manually entered IP addresses 2017-09-09 15:40:07 -07:00
Cameron Gutman 5b05220008 Prevent mDNS from overwriting external IP addresses 2017-09-09 15:21:31 -07:00
Cameron Gutman b2bd7257e1 Fix Lint warnings 2017-09-09 14:12:54 -07:00
Cameron Gutman 46a998c113 Convert address fields to strings to better manage DNS names 2017-09-09 13:39:54 -07:00
Cameron Gutman 60cd951774 Rename localIp/remoteIp fields to localAddress/remoteAddress to prepare for DNS names 2017-09-09 12:47:23 -07:00
Cameron Gutman d4f8d8f689 Switch database storage to use strings for addresses 2017-09-09 12:43:20 -07:00
Cameron Gutman 608a0ebb5b Update build files for AS3b5 2017-09-09 11:50:42 -07:00
Cameron Gutman f01a15d182 Removed duplicated current address logic 2017-09-09 11:49:15 -07:00
Cameron Gutman 0268b4f958 Update gradle for AS 3.0b4 2017-09-03 12:52:18 -07:00
Cameron Gutman d71cf0eb98 Add app category for Oreo 2017-09-02 13:48:45 -07:00
Cameron Gutman 10ab40f823 Add/update remaining assets 2017-09-02 13:48:11 -07:00
Cameron Gutman 427edfa021 Update common submodule 2017-09-01 19:11:49 -07:00
Cameron Gutman 6f18831d5c Update BouncyCastle libs 2017-09-01 18:39:49 -07:00
Cameron Gutman a3db09f422 Disable input compatibility mode on ChromeOS 2017-09-01 18:07:18 -07:00
Cameron Gutman d185a05b1d Sort and sync vendor IDs with xpad 2017-08-25 21:04:36 -07:00
Cameron Gutman 78e575504a Update straggling app icon 2017-08-23 23:07:03 -07:00
Cameron Gutman 0a0be19b69 Fix brown-paper-bag bug in audio init error checking 2017-08-22 00:17:03 -07:00
Cameron Gutman 0792157e9d Fix some markdown errors and tweak supported GPUs 2017-08-13 23:53:18 -07:00
madmario1000 cdd0ecf0b7 Update README.md (#400)
Clarify the required specs a bit
2017-08-13 23:49:49 -07:00
Cameron Gutman 1ac721a35b Bump to version 5.1.2 2017-08-13 18:51:22 -07:00
Cameron Gutman e49b1c92a2 Update for AS 3.0 Beta 2 2017-08-13 18:37:31 -07:00
Cameron Gutman db4295bf83 Add adaptive icon for PC shortcut 2017-08-13 18:31:09 -07:00
Cameron Gutman 824c37f9d5 Adaptive launcher icon 2017-08-13 18:06:53 -07:00
Cameron Gutman acf4426952 Update for Gradle 4 2017-06-24 12:56:52 -07:00
Cameron Gutman e8c50342ab Version 5.1.1 2017-06-17 16:06:57 -07:00
Cameron Gutman 598995de3b Fix audio renderer using non-existant classes on Lollipop 2017-06-16 20:27:03 -07:00
Cameron Gutman 01cf0cc649 Fix Lint error 2017-06-16 20:06:45 -07:00
Cameron Gutman fa560f462f Add battery saver mode 2017-06-16 20:01:41 -07:00
Cameron Gutman f6e40118a9 Bring back the warning displayed if video decoder initialization fails 2017-06-16 19:50:50 -07:00
Cameron Gutman fe7148dbd4 Only throw decoder exceptions if we're still receiving them after 3 seconds 2017-06-16 19:39:15 -07:00
Cameron Gutman 60de065836 Cleanup video decoder teardown paths 2017-06-16 19:11:39 -07:00
Cameron Gutman 6f82f82abb Use low latency audio pathway on Lollipop and later 2017-06-16 19:08:15 -07:00
71 changed files with 626 additions and 335 deletions
+10 -10
View File
@@ -1,4 +1,4 @@
#Moonlight # Moonlight
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield. [Moonlight](http://moonlight-stream.com) 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. We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
@@ -10,35 +10,35 @@ whether in your own home or over the internet.
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide. Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
##Features ## Features
* Streams any of your games from your PC to your Android device * Streams any of your games from your PC to your Android device
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield * Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network * Automatically finds GameStream-compatible PCs on your network
##Installation ## Installation
* Download and install Moonlight for Android from * 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), [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 * Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
##Requirements ## Requirements
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU * [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
* Android device running 4.1 (Jelly Bean) or higher * Android device running 4.1 (Jelly Bean) or higher
* High-end wireless router (802.11n dual-band recommended) * High-end wireless router (802.11n dual-band recommended)
##Usage ## Usage
* Turn on GameStream in the GFE settings * Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet * If you are connecting from outside the same network, turn on internet
streaming streaming
* When on the same network as your PC, open Moonlight and tap on your PC in the list * When on the same network as your PC, open Moonlight and tap on your PC in the list
* Accept the pairing confirmation on your PC * Accept the pairing confirmation on your PC and add the PIN if needed
* Tap your PC again to view the list of apps to stream * Tap your PC again to view the list of apps to stream
* Play games! * Play games!
##Contribute ## Contribute
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510) This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
@@ -46,13 +46,13 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
2. Write code 2. Write code
3. Send Pull Requests 3. Send Pull Requests
##Building ## Building
* Install Android Studio and the Android NDK * Install Android Studio and the Android NDK
* Run git submodule update --init --recursive from within moonlight-android/ * Run git submodule update --init --recursive from within moonlight-android/
* In moonlight-android/, create a file called local.properties. Add an ndk.dir= property to the local.properties file and set it equal to your NDK directory. * In moonlight-android/, create a file called local.properties. Add an ndk.dir= property to the local.properties file and set it equal to your NDK directory.
* Build the APK using Android Studio * Build the APK using Android Studio
##Authors ## Authors
* [Cameron Gutman](https://github.com/cgutman) * [Cameron Gutman](https://github.com/cgutman)
* [Diego Waxemberg](https://github.com/dwaxemberg) * [Diego Waxemberg](https://github.com/dwaxemberg)
+13 -8
View File
@@ -5,16 +5,18 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion 26 compileSdkVersion 26
buildToolsVersion '26.0.0' buildToolsVersion '26.0.1'
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 26 targetSdkVersion 26
versionName "5.1" versionName "5.2"
versionCode = 128 versionCode = 131
} }
flavorDimensions "root"
productFlavors { productFlavors {
root { root {
// Android O has native mouse capture, so don't show the rooted // Android O has native mouse capture, so don't show the rooted
@@ -25,6 +27,8 @@ android {
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64" abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
} }
dimension "root"
} }
nonRoot { nonRoot {
@@ -32,6 +36,8 @@ android {
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64" abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
} }
dimension "root"
} }
} }
@@ -54,10 +60,9 @@ android {
} }
dependencies { dependencies {
compile 'org.bouncycastle:bcprov-jdk15on:1.52' implementation 'org.bouncycastle:bcprov-jdk15on:1.57'
compile 'org.bouncycastle:bcpkix-jdk15on:1.52' implementation 'org.bouncycastle:bcpkix-jdk15on:1.57'
compile files('libs/jcodec-0.1.9-patched.jar') implementation files('libs/jcodec-0.1.9-patched.jar')
debugCompile project(path:':moonlight-common', configuration:'debug') implementation project(':moonlight-common')
releaseCompile project(path:':moonlight-common', configuration:'release')
} }
+8 -1
View File
@@ -24,12 +24,19 @@
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="false" /> android:required="false" />
<!-- Disable legacy input emulation on ChromeOS -->
<uses-feature
android:name="android.hardware.type.pc"
android:required="false"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:isGame="true" android:isGame="true"
android:banner="@drawable/atv_banner" android:banner="@drawable/atv_banner"
android:icon="@drawable/ic_launcher" android:appCategory="game"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<!-- Samsung multi-window support --> <!-- Samsung multi-window support -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+1 -3
View File
@@ -2,7 +2,6 @@ package com.limelight;
import java.io.StringReader; import java.io.StringReader;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerListener;
@@ -27,7 +26,6 @@ import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.ContextMenu; import android.view.ContextMenu;
@@ -250,7 +248,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
String computerName = getIntent().getStringExtra(NAME_EXTRA); String computerName = getIntent().getStringExtra(NAME_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+computerName; String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
TextView label = (TextView) findViewById(R.id.appListText); TextView label = findViewById(R.id.appListText);
setTitle(labelText); setTitle(labelText);
label.setText(labelText); label.setText(labelText);
+40 -9
View File
@@ -10,6 +10,7 @@ import com.limelight.binding.input.TouchContext;
import com.limelight.binding.input.driver.UsbDriverService; import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.binding.input.evdev.EvdevListener; import com.limelight.binding.input.evdev.EvdevListener;
import com.limelight.binding.input.virtual_controller.VirtualController; 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.MediaCodecDecoderRenderer;
import com.limelight.binding.video.MediaCodecHelper; import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.NvConnection;
@@ -34,6 +35,7 @@ import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.graphics.Point; import android.graphics.Point;
import android.hardware.input.InputManager; import android.hardware.input.InputManager;
import android.media.AudioManager; import android.media.AudioManager;
@@ -81,6 +83,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private VirtualController virtualController; private VirtualController virtualController;
private PreferenceConfiguration prefConfig; private PreferenceConfiguration prefConfig;
private SharedPreferences tombstonePrefs;
private NvConnection conn; private NvConnection conn;
private SpinnerDialog spinner; private SpinnerDialog spinner;
@@ -165,9 +168,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Read the stream preferences // Read the stream preferences
prefConfig = PreferenceConfiguration.readPreferences(this); prefConfig = PreferenceConfiguration.readPreferences(this);
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
// Listen for events on the game surface // Listen for events on the game surface
streamView = (StreamView) findViewById(R.id.surfaceView); streamView = findViewById(R.id.surfaceView);
streamView.setOnGenericMotionListener(this); streamView.setOnGenericMotionListener(this);
streamView.setOnTouchListener(this); streamView.setOnTouchListener(this);
@@ -214,7 +219,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the MediaCodec helper before creating the decoder // Initialize the MediaCodec helper before creating the decoder
MediaCodecHelper.initializeWithContext(this); MediaCodecHelper.initializeWithContext(this);
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat, prefConfig.bitrate); decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat,
prefConfig.bitrate,
prefConfig.batterySaver,
new CrashListener() {
@Override
public void notifyCrash(Exception e) {
// The MediaCodec instance is going down due to a crash
// let's tell the user something when they open the app again
// We must use commit because the app will crash when we return from this function
tombstonePrefs.edit().putInt("CrashCount", tombstonePrefs.getInt("CrashCount", 0) + 1).commit();
}
},
tombstonePrefs.getInt("CrashCount", 0));
// Display a message to the user if H.265 was forced on but we still didn't find a decoder // Display a message to the user if H.265 was forced on but we still didn't find a decoder
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) { if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
@@ -274,7 +292,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (prefConfig.onscreenController) { if (prefConfig.onscreenController) {
// create virtual onscreen controller // create virtual onscreen controller
virtualController = new VirtualController(conn, virtualController = new VirtualController(conn,
(FrameLayout)findViewById(R.id.surfaceView).getParent(), (FrameLayout)streamView.getParent(),
this); this);
virtualController.refreshLayout(); virtualController.refreshLayout();
} }
@@ -489,6 +507,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (message != null) { if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show(); Toast.makeText(this, message, Toast.LENGTH_LONG).show();
} }
// Clear the tombstone count
if (tombstonePrefs.getInt("CrashCount", 0) != 0) {
tombstonePrefs.edit().putInt("CrashCount", 0).apply();
}
} }
finish(); finish();
@@ -596,9 +619,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
boolean handled = false; boolean handled = false;
boolean detectedGamepad = event.getDevice() == null ? false : boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
if (detectedGamepad || (event.getDevice() == null || if (detectedGamepad || (event.getDevice() == null ||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
)) { )) {
@@ -638,9 +660,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
boolean handled = false; boolean handled = false;
boolean detectedGamepad = event.getDevice() == null ? false : boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
if (detectedGamepad || (event.getDevice() == null || if (detectedGamepad || (event.getDevice() == null ||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
)) { )) {
@@ -944,6 +965,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
displayedFailureDialog = true; displayedFailureDialog = true;
LimeLog.severe(stage+" failed: "+errorCode); LimeLog.severe(stage+" failed: "+errorCode);
// If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
}
});
}
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg)+" "+stage, true); getResources().getString(R.string.conn_error_msg)+" "+stage, true);
} }
@@ -51,14 +51,8 @@ public class HelpActivity extends Activity {
@Override @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) || return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase())) { url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
// Allow navigation to Moonlight docs
return false;
}
else {
return true;
}
} }
}); });
+33 -32
View File
@@ -2,9 +2,7 @@ package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider; import com.limelight.binding.crypto.AndroidCryptoProvider;
@@ -33,6 +31,7 @@ import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
@@ -110,9 +109,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Setup the list view // Setup the list view
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton); ImageButton settingsButton = findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc); ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
ImageButton helpButton = (ImageButton) findViewById(R.id.helpButton); ImageButton helpButton = findViewById(R.id.helpButton);
settingsButton.setOnClickListener(new OnClickListener() { settingsButton.setOnClickListener(new OnClickListener() {
@Override @Override
@@ -138,7 +137,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
.replace(R.id.pcFragmentContainer, new AdapterFragment()) .replace(R.id.pcFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss(); .commitAllowingStateLoss();
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout); noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) { if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE); noPcFoundLayout.setVisibility(View.VISIBLE);
} }
@@ -165,6 +164,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
PreferenceConfiguration.readPreferences(this).smallIconMode); PreferenceConfiguration.readPreferences(this).smallIconMode);
initializeViews(); initializeViews();
SharedPreferences prefs = getSharedPreferences("DecoderTombstone", 0);
int crashCount = prefs.getInt("CrashCount", 0);
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
// Remember the last crash count we notified at, so we don't
// display the crash dialog every time the app is started until
// they stream again
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
if (crashCount % 3 == 0) {
// At 3 consecutive crashes, we'll forcefully reset their settings
PreferenceConfiguration.resetStreamingSettings(this);
Dialog.displayDialog(this,
getResources().getString(R.string.title_decoding_reset),
getResources().getString(R.string.message_decoding_reset),
false);
}
else {
Dialog.displayDialog(this,
getResources().getString(R.string.title_decoding_error),
getResources().getString(R.string.message_decoding_error),
false);
}
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
}
} }
private void startComputerUpdates() { private void startComputerUpdates() {
@@ -306,19 +331,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Stop updates and wait while pairing // Stop updates and wait while pairing
stopComputerUpdates(true); stopComputerUpdates(true);
InetAddress addr; httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(), managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(), PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this)); PlatformBinding.getCryptoProvider(PcView.this));
@@ -443,19 +456,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
try { try {
InetAddress addr; httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(), managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(), PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this)); PlatformBinding.getCryptoProvider(PcView.this));
@@ -1,8 +1,10 @@
package com.limelight.binding.audio; package com.limelight.binding.audio;
import android.media.AudioAttributes;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.AudioTrack; import android.media.AudioTrack;
import android.os.Build;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer; import com.limelight.nvstream.av.audio.AudioRenderer;
@@ -12,10 +14,58 @@ public class AndroidAudioRenderer implements AudioRenderer {
private AudioTrack track; private AudioTrack track;
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return new AudioTrack(AudioManager.STREAM_MUSIC,
48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
}
else {
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME);
AudioFormat format = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(48000)
.setChannelMask(channelConfig)
.build();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Use FLAG_LOW_LATENCY on L through N
if (lowLatency) {
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
.setAudioFormat(format)
.setAudioAttributes(attributesBuilder.build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(bufferSize);
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
if (lowLatency) {
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
}
return trackBuilder.build();
}
else {
return new AudioTrack(attributesBuilder.build(),
format,
bufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE);
}
}
}
@Override @Override
public int setup(int audioConfiguration) { public int setup(int audioConfiguration) {
int channelConfig; int channelConfig;
int bufferSize;
int bytesPerFrame; int bytesPerFrame;
switch (audioConfiguration) switch (audioConfiguration)
@@ -38,44 +88,82 @@ public class AndroidAudioRenderer implements AudioRenderer {
// do this on many devices and it lowers audio latency. // do this on many devices and it lowers audio latency.
// We'll try the small buffer size first and if it fails, // We'll try the small buffer size first and if it fails,
// use the recommended larger buffer size. // use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = bytesPerFrame * 2;
track = new AudioTrack(AudioManager.STREAM_MUSIC, for (int i = 0; i < 4; i++) {
48000, boolean lowLatency;
channelConfig, int bufferSize;
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
try {
if (track != null) {
track.release();
}
} catch (Exception ignored) {}
// Now try the larger buffer size // We will try:
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000, // 1) Small buffer, low latency mode
// 2) Large buffer, low latency mode
// 3) Small buffer, standard mode
// 4) Large buffer, standard mode
switch (i) {
case 0:
case 1:
lowLatency = true;
break;
case 2:
case 3:
lowLatency = false;
break;
default:
// Unreachable
throw new IllegalStateException();
}
switch (i) {
case 0:
case 2:
bufferSize = bytesPerFrame * 2;
break;
case 1:
case 3:
// Try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
channelConfig, channelConfig,
AudioFormat.ENCODING_PCM_16BIT), AudioFormat.ENCODING_PCM_16BIT),
bytesPerFrame * 2); bytesPerFrame * 2);
// Round to next frame // Round to next frame
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame); bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
break;
default:
// Unreachable
throw new IllegalStateException();
}
track = new AudioTrack(AudioManager.STREAM_MUSIC, // Skip low latency options if hardware sample rate isn't 48000Hz
48000, if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
channelConfig, continue;
AudioFormat.ENCODING_PCM_16BIT, }
bufferSize,
AudioTrack.MODE_STREAM); try {
track.play(); track = createAudioTrack(channelConfig, bufferSize, lowLatency);
track.play();
// Successfully created working AudioTrack. We're done here.
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
break;
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
e.printStackTrace();
try {
if (track != null) {
track.release();
track = null;
}
} catch (Exception ignored) {}
}
}
if (track == null) {
// Couldn't create any audio track for playback
return -2;
} }
LimeLog.info("Audio track buffer size: "+bufferSize);
return 0; return 0;
} }
@@ -2,8 +2,6 @@ package com.limelight.binding.input;
import android.view.KeyEvent; import android.view.KeyEvent;
import com.limelight.nvstream.NvConnection;
/** /**
* Class to translate a Android key code into the codes GFE is expecting * Class to translate a Android key code into the codes GFE is expecting
* @author Diego Waxemberg * @author Diego Waxemberg
@@ -17,18 +17,22 @@ public class Xbox360Controller extends AbstractXboxController {
0x044f, // Thrustmaster 0x044f, // Thrustmaster
0x045e, // Microsoft 0x045e, // Microsoft
0x046d, // Logitech 0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz 0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // Unknown 0x0e6f, // Unknown
0x0f0d, // Hori
0x11c9, // Nacon
0x12ab, // Unknown 0x12ab, // Unknown
0x1430, // RedOctane 0x1430, // RedOctane
0x146b, // BigBen 0x146b, // BigBen
0x1bad, // Harmonix
0x0f0d, // Hori
0x1689, // Razer Onza
0x24c6, // PowerA
0x1532, // Razer Sabertooth 0x1532, // Razer Sabertooth
0x15e4, // Numark 0x15e4, // Numark
0x162e, // Joytech 0x162e, // Joytech
0x1689, // Razer Onza
0x1bad, // Harmonix
0x24c6, // PowerA
}; };
public static boolean canClaimDevice(UsbDevice device) { public static boolean canClaimDevice(UsbDevice device) {
@@ -19,6 +19,7 @@ public class XboxOneController extends AbstractXboxController {
0x0738, // Mad Catz 0x0738, // Mad Catz
0x0e6f, // Unknown 0x0e6f, // Unknown
0x0f0d, // Hori 0x0f0d, // Hori
0x1532, // Razer Wildcat
0x24c6, // PowerA 0x24c6, // PowerA
}; };
@@ -1,12 +1,12 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
public interface EvdevListener { public interface EvdevListener {
public static final int BUTTON_LEFT = 1; int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2; int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3; int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY); void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down); void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount); void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode); void keyboardEvent(boolean buttonDown, short keyCode);
} }
@@ -0,0 +1,5 @@
package com.limelight.binding.video;
public interface CrashListener {
void notifyCrash(Exception e);
}
@@ -12,6 +12,7 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.jni.MoonBridge; import com.limelight.nvstream.jni.MoonBridge;
import com.limelight.preferences.PreferenceConfiguration; import com.limelight.preferences.PreferenceConfiguration;
import android.content.SharedPreferences;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
@@ -47,10 +48,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private int videoFormat; private int videoFormat;
private SurfaceHolder renderTarget; private SurfaceHolder renderTarget;
private volatile boolean stopping; private volatile boolean stopping;
private CrashListener crashListener;
private boolean reportedCrash;
private int consecutiveCrashCount;
private boolean needsBaselineSpsHack; private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps; private SeqParameterSet savedSps;
private RendererException initialException;
private long initialExceptionTimestamp;
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
private long lastTimestampUs; private long lastTimestampUs;
private long decoderTimeMs; private long decoderTimeMs;
private long totalTimeMs; private long totalTimeMs;
@@ -105,12 +113,21 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
this.renderTarget = renderTarget; this.renderTarget = renderTarget;
} }
public MediaCodecDecoderRenderer(int videoFormat, int bitrate) { public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver,
CrashListener crashListener, int consecutiveCrashCount) {
//dumpDecoders(); //dumpDecoders();
this.bitrate = bitrate; this.bitrate = bitrate;
this.crashListener = crashListener;
this.consecutiveCrashCount = consecutiveCrashCount;
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()]; // Disable spinner threads in battery saver mode
if (batterySaver) {
spinnerThreads = new Thread[0];
}
else {
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
}
avcDecoder = findAvcDecoder(); avcDecoder = findAvcDecoder();
if (avcDecoder != null) { if (avcDecoder != null) {
@@ -294,13 +311,35 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
} }
// Only throw if the surface is still around and we're not stopping // Only throw if we're not stopping
if (!stopping && renderTarget.getSurface().isValid()) { if (!stopping) {
if (buf != null || codecFlags != 0) { //
throw new RendererException(this, e, buf, codecFlags); // There seems to be a race condition with decoder/surface teardown causing some
// decoders to to throw IllegalStateExceptions even before 'stopping' is set.
// To workaround this while allowing real exceptions to propagate, we will eat the
// first exception. If we are still receiving exceptions 3 seconds later, we will
// throw the original exception again.
//
if (initialException != null) {
// This isn't the first time we've had an exception processing video
if (System.currentTimeMillis() - initialExceptionTimestamp >= EXCEPTION_REPORT_DELAY_MS) {
// It's been over 3 seconds and we're still getting exceptions. Throw the original now.
if (!reportedCrash) {
reportedCrash = true;
crashListener.notifyCrash(initialException);
}
throw initialException;
}
} }
else { else {
throw new RendererException(this, e); // This is the first exception we've hit
if (buf != null || codecFlags != 0) {
initialException = new RendererException(this, e, buf, codecFlags);
}
else {
initialException = new RendererException(this, e);
}
initialExceptionTimestamp = System.currentTimeMillis();
} }
} }
} }
@@ -311,7 +350,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@Override @Override
public void run() { public void run() {
BufferInfo info = new BufferInfo(); BufferInfo info = new BufferInfo();
while (!isInterrupted()) { while (!stopping) {
try { try {
// Try to output a frame // Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000); int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
@@ -369,7 +408,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
public void run() { public void run() {
// This thread exists to keep the CPU at a higher DVFS state on devices // This thread exists to keep the CPU at a higher DVFS state on devices
// where the governor scales clock speed sporadically, causing dropped frames. // where the governor scales clock speed sporadically, causing dropped frames.
while (!isInterrupted()) { while (!stopping) {
try { try {
Thread.sleep(0, 1); Thread.sleep(0, 1);
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -418,17 +457,21 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
startSpinnerThreads(); startSpinnerThreads();
} }
// !!! May be called even if setup()/start() fails !!!
public void prepareForStop() { public void prepareForStop() {
// Let the decoding code know to ignore codec exceptions now // Let the decoding code know to ignore codec exceptions now
stopping = true; stopping = true;
// Halt the rendering thread
if (rendererThread != null) {
rendererThread.interrupt();
}
} }
@Override @Override
public void stop() { public void stop() {
stopping = true; // May be called already, but we'll call it now to be safe
prepareForStop();
// Halt the rendering thread
rendererThread.interrupt();
try { try {
// Invalidate pending decode buffers // Invalidate pending decode buffers
@@ -653,13 +696,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
inputBufferIndex = dequeueInputBuffer(); inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) { if (inputBufferIndex < 0) {
// We're being torn down now // We're being torn down now
return MoonBridge.DR_OK; return MoonBridge.DR_NEED_IDR;
} }
buf = getEmptyInputBuffer(inputBufferIndex); buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) { if (buf == null) {
// We're being torn down now // We're being torn down now
return MoonBridge.DR_OK; return MoonBridge.DR_NEED_IDR;
} }
// Write the annex B header // Write the annex B header
@@ -687,13 +730,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
inputBufferIndex = dequeueInputBuffer(); inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) { if (inputBufferIndex < 0) {
// We're being torn down now // We're being torn down now
return MoonBridge.DR_OK; return MoonBridge.DR_NEED_IDR;
} }
buf = getEmptyInputBuffer(inputBufferIndex); buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) { if (buf == null) {
// We're being torn down now // We're being torn down now
return MoonBridge.DR_OK; return MoonBridge.DR_NEED_IDR;
} }
if (needsBaselineSpsHack) { if (needsBaselineSpsHack) {
@@ -726,13 +769,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
inputBufferIndex = dequeueInputBuffer(); inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) { if (inputBufferIndex < 0) {
// We're being torn down now // We're being torn down now
return MoonBridge.DR_OK; return MoonBridge.DR_NEED_IDR;
} }
buf = getEmptyInputBuffer(inputBufferIndex); buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) { if (buf == null) {
// We're being torn down now // We're being torn down now
return MoonBridge.DR_OK; return MoonBridge.DR_NEED_IDR;
} }
// When we get the PPS, submit the VPS and SPS together with // When we get the PPS, submit the VPS and SPS together with
@@ -751,13 +794,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
inputBufferIndex = dequeueInputBuffer(); inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) { if (inputBufferIndex < 0) {
// We're being torn down now // We're being torn down now
return MoonBridge.DR_OK; return MoonBridge.DR_NEED_IDR;
} }
buf = getEmptyInputBuffer(inputBufferIndex); buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) { if (buf == null) {
// We're being torn down now // We're being torn down now
return MoonBridge.DR_OK; return MoonBridge.DR_NEED_IDR;
} }
} }
@@ -879,6 +922,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "Format: "+renderer.videoFormat+"\n"; str += "Format: "+renderer.videoFormat+"\n";
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n"; str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n"; str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n"; str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "FPS target: "+renderer.refreshRate+"\n"; str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.bitrate+" Mbps \n"; str += "Bitrate: "+renderer.bitrate+" Mbps \n";
@@ -900,6 +945,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "Is Exynos 4: "+renderer.isExynos4+"\n"; str += "Is Exynos 4: "+renderer.isExynos4+"\n";
str += "/proc/cpuinfo:\n";
try {
str += MediaCodecHelper.readCpuinfo();
} catch (Exception e) {
str += e.getMessage();
}
str += "Full decoder dump:\n"; str += "Full decoder dump:\n";
try { try {
str += MediaCodecHelper.dumpDecoders(); str += MediaCodecHelper.dumpDecoders();
@@ -25,6 +25,8 @@ public class ComputerDatabaseManager {
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp"; private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac"; private static final String MAC_COLUMN_NAME = "Mac";
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
private SQLiteDatabase computerDb; private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) { public ComputerDatabaseManager(Context c) {
@@ -60,50 +62,74 @@ public class ComputerDatabaseManager {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name); values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString()); values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress()); values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress()); values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
values.put(MAC_COLUMN_NAME, details.macAddress); values.put(MAC_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
private ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
// An earlier schema defined addresses as byte blobs. We'll
// gracefully migrate those to strings so we can store DNS names
// too. To disambiguate, we'll need to prefix them with a string
// greater than the allowable IP address length.
try {
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
LimeLog.warning("DB: Legacy local address for "+details.name);
} catch (UnknownHostException e) {
// This is probably a hostname/address with the prefix string
String stringData = c.getString(2);
if (stringData.startsWith(ADDRESS_PREFIX)) {
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
}
else {
LimeLog.severe("DB: Corrupted local address for "+details.name);
}
}
try {
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
LimeLog.warning("DB: Legacy remote address for "+details.name);
} catch (UnknownHostException e) {
// This is probably a hostname/address with the prefix string
String stringData = c.getString(3);
if (stringData.startsWith(ADDRESS_PREFIX)) {
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
}
else {
LimeLog.severe("DB: Corrupted local address for "+details.name);
}
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
return details;
}
public List<ComputerDetails> getAllComputers() { public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null); Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>(); LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) { while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = getComputerFromCursor(c);
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, skip the database entry // If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null || if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
details.macAddress == null) { details.macAddress == null) {
continue; continue;
} }
@@ -119,46 +145,17 @@ public class ComputerDatabaseManager {
public ComputerDetails getComputerByName(String name) { public ComputerDetails getComputerByName(String name) {
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null); Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
ComputerDetails details = new ComputerDetails();
if (!c.moveToFirst()) { if (!c.moveToFirst()) {
// No matching computer // No matching computer
c.close(); c.close();
return null; return null;
} }
details.name = c.getString(0); ComputerDetails details = getComputerFromCursor(c);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
c.close(); c.close();
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, delete the database entry // If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null || if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
details.macAddress == null) { details.macAddress == null) {
deleteComputer(details.name); deleteComputer(details.name);
return null; return null;
@@ -3,5 +3,5 @@ package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
public interface ComputerManagerListener { public interface ComputerManagerListener {
public void notifyComputerUpdated(ComputerDetails details); void notifyComputerUpdated(ComputerDetails details);
} }
@@ -3,7 +3,6 @@ package com.limelight.computers;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.StringReader; import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.util.LinkedList; import java.util.LinkedList;
@@ -20,6 +19,7 @@ import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper; import com.limelight.utils.CacheHelper;
import com.limelight.utils.ServerHelper;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
@@ -154,7 +154,7 @@ public class ComputerManagerService extends Service {
} }
} }
}; };
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress()); t.setName("Polling thread for " + tuple.computer.localAddress);
return t; return t;
} }
@@ -210,8 +210,8 @@ public class ComputerManagerService extends Service {
} }
} }
public boolean addComputerBlocking(InetAddress addr) { public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
return ComputerManagerService.this.addComputerBlocking(addr); return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
} }
public void removeComputer(String name) { public void removeComputer(String name) {
@@ -289,7 +289,7 @@ public class ComputerManagerService extends Service {
@Override @Override
public void notifyComputerAdded(MdnsComputer computer) { public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine // Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress()); addComputerBlocking(computer.getAddress().getHostAddress(), false);
} }
@Override @Override
@@ -305,15 +305,22 @@ public class ComputerManagerService extends Service {
}; };
} }
private void addTuple(ComputerDetails details) { private void addTuple(ComputerDetails details, boolean manuallyAdded) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer // Check if this is the same computer
if (tuple.computer.uuid.equals(details.uuid)) { if (tuple.computer.uuid.equals(details.uuid)) {
// Update details anyway in case this machine has been re-added by IP if (manuallyAdded) {
// after not being reachable by our existing information // Update details anyway in case this machine has been re-added by IP
tuple.computer.localIp = details.localIp; // after not being reachable by our existing information
tuple.computer.remoteIp = details.remoteIp; tuple.computer.localAddress = details.localAddress;
tuple.computer.remoteAddress = details.remoteAddress;
}
else {
// This indicates that mDNS discovered this address, so we
// should only apply the local address.
tuple.computer.localAddress = details.localAddress;
}
// Start a polling thread if polling is active // Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) { if (pollingActive && tuple.thread == null) {
@@ -338,11 +345,11 @@ public class ComputerManagerService extends Service {
} }
} }
public boolean addComputerBlocking(InetAddress addr) { public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
// Setup a placeholder // Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails(); ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr; fakeDetails.localAddress = addr;
fakeDetails.remoteIp = addr; fakeDetails.remoteAddress = addr;
// Block while we try to fill the details // Block while we try to fill the details
try { try {
@@ -356,7 +363,7 @@ public class ComputerManagerService extends Service {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid); LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine // Start a polling thread for this machine
addTuple(fakeDetails); addTuple(fakeDetails, manuallyAdded);
return true; return true;
} }
else { else {
@@ -404,14 +411,14 @@ public class ComputerManagerService extends Service {
} }
} }
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) { private ComputerDetails tryPollIp(ComputerDetails details, String address) {
// Fast poll this address first to determine if we can connect at the TCP layer // Fast poll this address first to determine if we can connect at the TCP layer
if (!fastPollIp(ipAddr)) { if (!fastPollIp(address)) {
return null; return null;
} }
try { try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(), NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails(); ComputerDetails newDetails = http.getComputerDetails();
@@ -432,10 +439,10 @@ public class ComputerManagerService extends Service {
// Just try to establish a TCP connection to speculatively detect a running // Just try to establish a TCP connection to speculatively detect a running
// GFE server // GFE server
private boolean fastPollIp(InetAddress addr) { private boolean fastPollIp(String address) {
Socket s = new Socket(); Socket s = new Socket();
try { try {
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT); s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
s.close(); s.close();
return true; return true;
} catch (IOException e) { } catch (IOException e) {
@@ -443,11 +450,11 @@ public class ComputerManagerService extends Service {
} }
} }
private void startFastPollThread(final InetAddress addr, final boolean[] info) { private void startFastPollThread(final String address, final boolean[] info) {
Thread t = new Thread() { Thread t = new Thread() {
@Override @Override
public void run() { public void run() {
boolean pollRes = fastPollIp(addr); boolean pollRes = fastPollIp(address);
synchronized (info) { synchronized (info) {
info[0] = true; // Done info[0] = true; // Done
@@ -457,16 +464,16 @@ public class ComputerManagerService extends Service {
} }
} }
}; };
t.setName("Fast Poll - "+addr.getHostAddress()); t.setName("Fast Poll - "+address);
t.start(); t.start();
} }
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException { private ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2]; final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2]; final boolean[] localInfo = new boolean[2];
startFastPollThread(local, localInfo); startFastPollThread(localAddress, localInfo);
startFastPollThread(remote, remoteInfo); startFastPollThread(remoteAddress, remoteInfo);
// Check local first // Check local first
synchronized (localInfo) { synchronized (localInfo) {
@@ -499,13 +506,13 @@ public class ComputerManagerService extends Service {
// If the local address is routable across the Internet, // If the local address is routable across the Internet,
// always consider this PC remote to be conservative // always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) { if (details.localAddress.equals(details.remoteAddress)) {
reachability = ComputerDetails.Reachability.REMOTE; reachability = ComputerDetails.Reachability.REMOTE;
} }
else { else {
// Do a TCP-level connection to the HTTP server to see if it's listening // Do a TCP-level connection to the HTTP server to see if it's listening
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")"); LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +")");
reachability = fastPollPc(details.localIp, details.remoteIp); reachability = fastPollPc(details.localAddress, details.remoteAddress);
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString()); LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
// If no connection could be established to either IP address, there's nothing we can do // If no connection could be established to either IP address, there's nothing we can do
@@ -517,39 +524,39 @@ public class ComputerManagerService extends Service {
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL); boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) { if (localFirst) {
polledDetails = tryPollIp(details, details.localIp); polledDetails = tryPollIp(details, details.localAddress);
} }
else { else {
polledDetails = tryPollIp(details, details.remoteIp); polledDetails = tryPollIp(details, details.remoteAddress);
} }
InetAddress reachableAddr = null; String reachableAddr = null;
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
// Failed, so let's try the fallback // Failed, so let's try the fallback
if (!localFirst) { if (!localFirst) {
polledDetails = tryPollIp(details, details.localIp); polledDetails = tryPollIp(details, details.localAddress);
} }
else { else {
polledDetails = tryPollIp(details, details.remoteIp); polledDetails = tryPollIp(details, details.remoteAddress);
} }
if (polledDetails != null) { if (polledDetails != null) {
// The fallback poll worked // The fallback poll worked
reachableAddr = !localFirst ? details.localIp : details.remoteIp; reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
} }
} }
else if (polledDetails != null) { else if (polledDetails != null) {
reachableAddr = localFirst ? details.localIp : details.remoteIp; reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
} }
if (reachableAddr == null) { if (reachableAddr == null) {
return null; return null;
} }
if (polledDetails.remoteIp.equals(reachableAddr)) { if (polledDetails.remoteAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.REMOTE; polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
} }
else if (polledDetails.localIp.equals(reachableAddr)) { else if (polledDetails.localAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.LOCAL; polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
} }
else { else {
@@ -571,7 +578,7 @@ public class ComputerManagerService extends Service {
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer); ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
if (confirmationReachTuple == null) { if (confirmationReachTuple == null) {
// Neither of those seem to work, so we'll hold onto the address that did work // Neither of those seem to work, so we'll hold onto the address that did work
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress; initialReachTuple.computer.localAddress = initialReachTuple.reachableAddress;
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL; initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
} }
else { else {
@@ -581,8 +588,11 @@ public class ComputerManagerService extends Service {
} }
} }
// Save the old MAC address // Save some details about the old state of the PC that we may wish
// to restore later.
String savedMacAddress = details.macAddress; String savedMacAddress = details.macAddress;
String savedLocalAddress = details.localAddress;
String savedRemoteAddress = details.remoteAddress;
// If we got here, it's reachable // If we got here, it's reachable
details.update(initialReachTuple.computer); details.update(initialReachTuple.computer);
@@ -593,6 +603,33 @@ public class ComputerManagerService extends Service {
details.macAddress = savedMacAddress; details.macAddress = savedMacAddress;
} }
// We never want to lose IP addresses by polling server info. If we get a poll back
// where localAddress == remoteAddress but savedLocalAddress != savedRemoteAddress,
// then we've lost an address in the polling and we should restore the one that's missing.
if (details.localAddress.equals(details.remoteAddress) &&
!savedLocalAddress.equals(savedRemoteAddress)) {
if (details.localAddress.equals(savedLocalAddress)) {
// Local addresses are identical, so put the old remote address back
details.remoteAddress = savedRemoteAddress;
}
else if (details.remoteAddress.equals(savedRemoteAddress)) {
// Remote addresses are identical, so put the old local address back
details.localAddress = savedLocalAddress;
}
else {
// Neither IP address match. Let's restore the remote address to be safe.
details.remoteAddress = savedRemoteAddress;
}
// Now update the reachability so the correct address is used
if (details.localAddress.equals(initialReachTuple.reachableAddress)) {
details.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
details.reachability = ComputerDetails.Reachability.REMOTE;
}
}
return true; return true;
} }
@@ -616,7 +653,7 @@ public class ComputerManagerService extends Service {
for (ComputerDetails computer : dbManager.getAllComputers()) { for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add tuples for each computer // Add tuples for each computer
addTuple(computer); addTuple(computer, true);
} }
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
@@ -694,8 +731,6 @@ public class ComputerManagerService extends Service {
public void run() { public void run() {
int emptyAppListResponses = 0; int emptyAppListResponses = 0;
do { do {
InetAddress selectedAddr;
// Can't poll if it's not online // Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) { if (computer.state != ComputerDetails.State.ONLINE) {
if (listener != null) { if (listener != null) {
@@ -709,19 +744,12 @@ public class ComputerManagerService extends Service {
continue; continue;
} }
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
selectedAddr = computer.localIp;
}
else {
selectedAddr = computer.remoteIp;
}
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
PollingTuple tuple = getPollingTuple(computer); PollingTuple tuple = getPollingTuple(computer);
try { try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
String appList; String appList;
if (tuple != null) { if (tuple != null) {
// If we're polling this machine too, grab the network lock // If we're polling this machine too, grab the network lock
@@ -816,10 +844,10 @@ class PollingTuple {
} }
class ReachabilityTuple { class ReachabilityTuple {
public final InetAddress reachableAddress; public final String reachableAddress;
public final ComputerDetails computer; public final ComputerDetails computer;
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) { public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
this.computer = computer; this.computer = computer;
this.reachableAddress = reachableAddress; this.reachableAddress = reachableAddress;
} }
@@ -55,10 +55,10 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
convertView = inflater.inflate(layoutId, viewGroup, false); convertView = inflater.inflate(layoutId, viewGroup, false);
} }
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image); ImageView imgView = convertView.findViewById(R.id.grid_image);
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay); ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text); TextView txtView = convertView.findViewById(R.id.grid_text);
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner); ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
if (imgView != null) { if (imgView != null) {
if (!populateImageView(imgView, prgView, itemList.get(i))) { if (!populateImageView(imgView, prgView, itemList.get(i))) {
@@ -4,12 +4,11 @@ import android.content.Context;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.utils.ServerHelper;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.InetAddress;
public class NetworkAssetLoader { public class NetworkAssetLoader {
private final Context context; private final Context context;
@@ -21,10 +20,9 @@ public class NetworkAssetLoader {
} }
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) { public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
InputStream in = null; InputStream in = null;
try { try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
in = http.getBoxArt(tuple.app); in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {} } catch (IOException ignored) {}
@@ -37,13 +35,4 @@ public class NetworkAssetLoader {
return in; return in;
} }
private static InetAddress getCurrentAddress(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
} }
@@ -1,8 +1,5 @@
package com.limelight.preferences; package com.limelight.preferences;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
@@ -17,7 +14,6 @@ import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -50,18 +46,12 @@ public class AddComputerManually extends Activity {
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc), SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false); getResources().getString(R.string.msg_add_pc), false);
try { if (!managerBinder.addComputerBlocking(host, true)){
InetAddress addr = InetAddress.getByName(host); msg = getResources().getString(R.string.addpc_fail);
}
if (!managerBinder.addComputerBlocking(addr)){ else {
msg = getResources().getString(R.string.addpc_fail); msg = getResources().getString(R.string.addpc_success);
} finish = true;
else {
msg = getResources().getString(R.string.addpc_success);
finish = true;
}
} catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host);
} }
dialog.dismiss(); dialog.dismiss();
@@ -142,7 +132,7 @@ public class AddComputerManually extends Activity {
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
this.hostText = (TextView) findViewById(R.id.hostTextView); this.hostText = findViewById(R.id.hostTextView);
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE); hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() { hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override @Override
@@ -3,7 +3,6 @@ package com.limelight.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Build; import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
@@ -23,6 +22,7 @@ public class PreferenceConfiguration {
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver"; private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final String VIDEO_FORMAT_PREF_STRING = "video_format"; private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls"; private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -45,6 +45,7 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_USB_DRIVER = true; private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto"; private static final String DEFAULT_VIDEO_FORMAT = "auto";
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false; private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
private static final boolean DEFAULT_BATTERY_SAVER = false;
public static final int FORCE_H265_ON = -1; public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0; public static final int AUTOSELECT_H265 = 0;
@@ -58,6 +59,7 @@ public class PreferenceConfiguration {
public String language; public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver; public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean onscreenController; public boolean onscreenController;
public boolean batterySaver;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@@ -128,6 +130,16 @@ public class PreferenceConfiguration {
} }
} }
public static void resetStreamingSettings(Context context) {
// We consider resolution, FPS, bitrate, and video format as "streaming settings" here
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit()
.remove(BITRATE_PREF_STRING)
.remove(RES_FPS_PREF_STRING)
.remove(VIDEO_FORMAT_PREF_STRING)
.apply();
}
public static PreferenceConfiguration readPreferences(Context context) { public static PreferenceConfiguration readPreferences(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration(); PreferenceConfiguration config = new PreferenceConfiguration();
@@ -188,6 +200,7 @@ public class PreferenceConfiguration {
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND); config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER); config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT); config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
return config; return config;
} }
@@ -2,7 +2,6 @@ package com.limelight.preferences;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
import android.preference.Preference; import android.preference.Preference;
@@ -15,8 +14,6 @@ import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity { public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs; private PreferenceConfiguration previousPrefs;
@@ -3,6 +3,6 @@ package com.limelight.ui;
import android.widget.AbsListView; import android.widget.AbsListView;
public interface AdapterFragmentCallbacks { public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId(); int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView); void receiveAbsListView(AbsListView gridView);
} }
@@ -1,5 +1,5 @@
package com.limelight.ui; package com.limelight.ui;
public interface GameGestures { public interface GameGestures {
public void showKeyboard(); void showKeyboard();
} }
@@ -1,6 +1,5 @@
package com.limelight.utils; package com.limelight.utils;
import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@@ -14,21 +14,18 @@ import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
public class ServerHelper { public class ServerHelper {
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) { public static String getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ? return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp; computer.localAddress : computer.remoteAddress;
} }
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer, public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) { ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class); Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST, intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName()); intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId()); intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
@@ -45,7 +42,7 @@ public class ServerHelper {
} }
public static void doQuit(final Activity parent, public static void doQuit(final Activity parent,
final InetAddress address, final String address,
final NvApp app, final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder, final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) { final Runnable onComplete) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 14 KiB

@@ -6,7 +6,7 @@
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AddComputerManually" > tools:context=".preferences.AddComputerManually" >
<TextView <TextView
android:id="@+id/manuallyAddPcText" android:id="@+id/manuallyAddPcText"
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_help"
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="com.limelight.HelpActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
</RelativeLayout>
@@ -7,6 +7,6 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
android:id="@+id/stream_settings" android:id="@+id/stream_settings"
tools:context=".StreamSettings"> tools:context=".preferences.StreamSettings">
</RelativeLayout> </RelativeLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_pc_scut_background"/>
<foreground android:drawable="@mipmap/ic_pc_scut_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#565C64</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_pc_scut_background">#FFFFFF</color>
</resources>
+6
View File
@@ -47,6 +47,10 @@
<string name="error_404">GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. <string name="error_404">GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU.
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE. Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
</string> </string>
<string name="title_decoding_error">Video Decoder Crashed</string>
<string name="message_decoding_error">Moonlight has crashed due to a problem with this device\'s video decoder. Try adjusting the streaming settings if the crashes continue.</string>
<string name="title_decoding_reset">Video Settings Reset</string>
<string name="message_decoding_reset">Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default.</string>
<!-- Start application messages --> <!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string> <string name="conn_establishing_title">Establishing Connection</string>
@@ -104,6 +108,8 @@
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string> <string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string> <string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string> <string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="title_checkbox_battery_saver">Battery saver</string>
<string name="summary_checkbox_battery_saver">Uses less battery, but may increase stuttering</string>
<string name="category_audio_settings">Audio Settings</string> <string name="category_audio_settings">Audio Settings</string>
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string> <string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
+3 -3
View File
@@ -21,9 +21,9 @@
android:title="@string/title_checkbox_stretch_video" android:title="@string/title_checkbox_stretch_video"
android:defaultValue="false" /> android:defaultValue="false" />
<CheckBoxPreference <CheckBoxPreference
android:key="checkbox_disable_warnings" android:key="checkbox_battery_saver"
android:title="@string/title_checkbox_disable_warnings" android:title="@string/title_checkbox_battery_saver"
android:summary="@string/summary_checkbox_disable_warnings" android:summary="@string/summary_checkbox_battery_saver"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/category_audio_settings"> <PreferenceCategory android:title="@string/category_audio_settings">
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.3.3' classpath 'com.android.tools.build:gradle:3.0.0-beta5'
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
#Thu Mar 02 18:32:01 PST 2017 #Sun Sep 03 12:51:12 PDT 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> <module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager"> <component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle"> <facet type="java-gradle" name="Java-Gradle">
<configuration> <configuration>
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+92
View File
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.0"
width="546.083726pt"
height="546.155759pt"
viewBox="0 0 546.083726 546.155759"
preserveAspectRatio="xMidYMid meet"
id="svg4059"
sodipodi:docname="lime_layer.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<defs
id="defs4063" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="951"
id="namedview4061"
showgrid="false"
inkscape:zoom="0.32408337"
inkscape:cx="364.05583"
inkscape:cy="364.10384"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4059" />
<metadata
id="metadata4039">
Created by potrace 1.12, written by Peter Selinger 2001-2015
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(-202.416274,747.655759) scale(0.100000,-0.100000)"
fill="#000000"
stroke="none"
id="g4057"
style="fill:#ffffff">
<path
d="M4550 7473 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z"
id="path4041"
style="fill:#ffffff" />
<path
d="M4867 7473 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z"
id="path4043"
style="fill:#ffffff" />
<path
d="M2673 6518 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z"
id="path4045"
style="fill:#ffffff" />
<path
d="M5898 5733 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z"
id="path4047"
style="fill:#ffffff" />
<path
d="M2026 4631 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z"
id="path4049"
style="fill:#ffffff" />
<path
d="M5030 4633 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z"
id="path4051"
style="fill:#ffffff" />
<path
d="M3767 3602 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z"
id="path4053"
style="fill:#ffffff" />
<path
d="M4862 3243 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z"
id="path4055"
style="fill:#ffffff" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB