Compare commits

..
1 Commits
80 changed files with 445 additions and 937 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 an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported) * [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
* 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 and add the PIN if needed * Accept the pairing confirmation on your PC
* 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)
+9 -20
View File
@@ -4,31 +4,23 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 26 compileSdkVersion 25
buildToolsVersion '26.0.1' buildToolsVersion '25.0.3'
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 26 targetSdkVersion 25
versionName "5.2" versionName "5.0.1"
versionCode = 131 versionCode = 123
} }
flavorDimensions "root"
productFlavors { productFlavors {
root { root {
// Android O has native mouse capture, so don't show the rooted
// version to devices running O on the Play Store.
maxSdkVersion 25
applicationId "com.limelight.root" applicationId "com.limelight.root"
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 {
@@ -36,8 +28,6 @@ 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"
} }
} }
@@ -60,9 +50,8 @@ android {
} }
dependencies { dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.57' compile 'org.bouncycastle:bcprov-jdk15on:1.52'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.57' compile 'org.bouncycastle:bcpkix-jdk15on:1.52'
implementation files('libs/jcodec-0.1.9-patched.jar') compile project(':moonlight-common')
compile files('libs/jcodec-0.1.9-patched.jar')
implementation project(':moonlight-common')
} }
+1 -8
View File
@@ -24,19 +24,12 @@
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:appCategory="game" android:icon="@drawable/ic_launcher"
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.

Before

Width:  |  Height:  |  Size: 58 KiB

+3 -1
View File
@@ -2,6 +2,7 @@ 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;
@@ -26,6 +27,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.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;
@@ -248,7 +250,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 = findViewById(R.id.appListText); TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText); setTitle(labelText);
label.setText(labelText); label.setText(labelText);
+36 -96
View File
@@ -10,7 +10,6 @@ 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;
@@ -35,7 +34,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.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;
@@ -83,7 +81,6 @@ 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;
@@ -168,28 +165,12 @@ 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 = findViewById(R.id.surfaceView); streamView = (StreamView) findViewById(R.id.surfaceView);
streamView.setOnGenericMotionListener(this); streamView.setOnGenericMotionListener(this);
streamView.setOnTouchListener(this); streamView.setOnTouchListener(this);
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// The view must be focusable for pointer capture to work.
streamView.setFocusable(true);
streamView.setDefaultFocusHighlightEnabled(false);
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
@Override
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
return handleMotionEvent(motionEvent);
}
});
}
// Warn the user if they're on a metered connection // Warn the user if they're on a metered connection
checkDataConnection(); checkDataConnection();
@@ -219,20 +200,7 @@ 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, 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()) {
@@ -289,10 +257,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
getWindow().setSustainedPerformanceMode(true); getWindow().setSustainedPerformanceMode(true);
} }
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
if (prefConfig.onscreenController) { if (prefConfig.onscreenController) {
// create virtual onscreen controller // create virtual onscreen controller
virtualController = new VirtualController(conn, virtualController = new VirtualController(conn,
(FrameLayout)streamView.getParent(), (FrameLayout)findViewById(R.id.surfaceView).getParent(),
this); this);
virtualController.refreshLayout(); virtualController.refreshLayout();
} }
@@ -307,20 +277,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.getHolder().addCallback(this); streamView.getHolder().addCallback(this);
} }
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 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();
}
}
}
private void prepareDisplayForRendering() { private void prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay(); Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes(); WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
@@ -464,17 +420,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
unbindService(usbDriverServiceConnection); unbindService(usbDriverServiceConnection);
} }
// Destroy the capture provider
inputCaptureProvider.destroy();
}
@Override
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (conn != null) { if (conn != null) {
int videoFormat = decoderRenderer.getActiveVideoFormat(); int videoFormat = decoderRenderer.getActiveVideoFormat();
@@ -507,12 +452,15 @@ 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();
}
} }
}
@Override
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
finish(); finish();
} }
@@ -619,8 +567,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
boolean handled = false; boolean handled = false;
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || boolean detectedGamepad = event.getDevice() == null ? false :
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD); ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(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
)) { )) {
@@ -660,8 +609,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
boolean handled = false; boolean handled = false;
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || boolean detectedGamepad = event.getDevice() == null ? false :
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD); ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(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
)) { )) {
@@ -721,8 +671,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true; return true;
} }
} }
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 || else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
{ {
// This case is for mice // This case is for mice
if (event.getSource() == InputDevice.SOURCE_MOUSE || if (event.getSource() == InputDevice.SOURCE_MOUSE ||
@@ -731,11 +680,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{ {
int changedButtons = event.getButtonState() ^ lastButtonState; int changedButtons = event.getButtonState() ^ lastButtonState;
// Ignore mouse input if we're not capturing from our input source
if (!inputCaptureProvider.isCapturingActive()) {
return false;
}
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) { if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet // Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL); byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
@@ -949,6 +893,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
connecting = connected = false; connecting = connected = false;
conn.stop(); conn.stop();
} }
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
// Destroy the capture provider
inputCaptureProvider.destroy();
} }
@Override @Override
@@ -958,23 +908,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
spinner = null; spinner = null;
} }
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
if (!displayedFailureDialog) { if (!displayedFailureDialog) {
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 stopConnection();
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);
} }
@@ -982,14 +920,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override @Override
public void connectionTerminated(long errorCode) { public void connectionTerminated(long errorCode) {
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
if (!displayedFailureDialog) { if (!displayedFailureDialog) {
displayedFailureDialog = true; displayedFailureDialog = true;
LimeLog.severe("Connection terminated: "+errorCode); LimeLog.severe("Connection terminated: "+errorCode);
stopConnection();
stopConnection();
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title), Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true); getResources().getString(R.string.conn_terminated_msg), true);
} }
@@ -1056,10 +991,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
// Let the decoder know immediately that the surface is gone
decoderRenderer.prepareForStop();
if (connected) { if (connected) {
// HACK: Android is supposed to let you return from this function
// before throwing a fit if you access the surface again. Unfortunately,
// MediaCodec often tries to access the destroyed surface and triggers
// an IllegalStateException. To workaround this, we will invoke
// the DecoderRenderer's stop function ourselves, so it will hopefully
// happen early enough to not trigger the bug
decoderRenderer.stop();
stopConnection(); stopConnection();
} }
} }
@@ -51,8 +51,14 @@ public class HelpActivity extends Activity {
@Override @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) || if (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;
}
} }
}); });
+32 -33
View File
@@ -2,7 +2,9 @@ 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;
@@ -31,7 +33,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.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;
@@ -109,9 +110,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 = findViewById(R.id.settingsButton); ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc); ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
ImageButton helpButton = findViewById(R.id.helpButton); ImageButton helpButton = (ImageButton) findViewById(R.id.helpButton);
settingsButton.setOnClickListener(new OnClickListener() { settingsButton.setOnClickListener(new OnClickListener() {
@Override @Override
@@ -137,7 +138,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
.replace(R.id.pcFragmentContainer, new AdapterFragment()) .replace(R.id.pcFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss(); .commitAllowingStateLoss();
noPcFoundLayout = findViewById(R.id.no_pc_found_layout); noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) { if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE); noPcFoundLayout.setVisibility(View.VISIBLE);
} }
@@ -164,32 +165,6 @@ 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() {
@@ -331,7 +306,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Stop updates and wait while pairing // Stop updates and wait while pairing
stopComputerUpdates(true); stopComputerUpdates(true);
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), InetAddress addr;
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));
@@ -456,7 +443,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
try { try {
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), InetAddress addr;
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,10 +1,8 @@
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;
@@ -14,58 +12,10 @@ 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)
@@ -88,82 +38,44 @@ 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;
for (int i = 0; i < 4; i++) { track = new AudioTrack(AudioManager.STREAM_MUSIC,
boolean lowLatency; 48000,
int bufferSize; channelConfig,
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) {}
// We will try: // Now try the larger buffer size
// 1) Small buffer, low latency mode bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
// 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();
}
// Skip low latency options if hardware sample rate isn't 48000Hz track = new AudioTrack(AudioManager.STREAM_MUSIC,
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) { 48000,
continue; channelConfig,
} AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
try { AudioTrack.MODE_STREAM);
track = createAudioTrack(channelConfig, bufferSize, lowLatency); track.play();
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;
} }
@@ -63,10 +63,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
int[] ids = InputDevice.getDeviceIds(); int[] ids = InputDevice.getDeviceIds();
for (int id : ids) { for (int id : ids) {
InputDevice dev = InputDevice.getDevice(id); InputDevice dev = InputDevice.getDevice(id);
if (dev == null) {
// This device was removed during enumeration
continue;
}
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 || if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) { (dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure // This looks like a gamepad, but we'll check X and Y to be sure
@@ -2,6 +2,8 @@ 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
@@ -10,11 +10,11 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider { public class AndroidCaptureProvider extends InputCaptureProvider {
private ViewGroup rootViewGroup; private ViewGroup rootViewGroup;
private Context context; private Context context;
public AndroidPointerIconCaptureProvider(Activity activity) { public AndroidCaptureProvider(Activity activity) {
this.context = activity; this.context = activity;
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView(); this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
} }
@@ -33,13 +33,11 @@ public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
@Override @Override
public void enableCapture() { public void enableCapture() {
super.enableCapture();
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL)); setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
} }
@Override @Override
public void disableCapture() { public void disableCapture() {
super.disableCapture();
setPointerIconOnAllViews(null); setPointerIconOnAllViews(null);
} }
@@ -1,53 +0,0 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
@TargetApi(Build.VERSION_CODES.O)
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
private View targetView;
public AndroidNativePointerCaptureProvider(View targetView) {
this.targetView = targetView;
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
@Override
public void enableCapture() {
super.enableCapture();
targetView.requestPointerCapture();
}
@Override
public void disableCapture() {
super.disableCapture();
targetView.releasePointerCapture();
}
@Override
public boolean isCapturingActive() {
return targetView.hasPointerCapture();
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getX();
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getY();
}
}
@@ -3,17 +3,15 @@ package com.limelight.binding.input.capture;
import android.app.Activity; import android.app.Activity;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.input.evdev.EvdevCaptureProvider; import com.limelight.binding.input.evdev.EvdevCaptureProvider;
import com.limelight.binding.input.evdev.EvdevListener; import com.limelight.binding.input.evdev.EvdevListener;
public class InputCaptureManager { public class InputCaptureManager {
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) { public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) { // Shield capture is preferred because it can capture when the cursor is over
LimeLog.info("Using Android O+ native mouse capture"); // the system UI. Android N native capture can only capture over views owned
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView)); // by the application.
} if (ShieldCaptureProvider.isCaptureProviderSupported()) {
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension"); LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity); return new ShieldCaptureProvider(activity);
} }
@@ -21,11 +19,9 @@ public class InputCaptureManager {
LimeLog.info("Using Evdev mouse capture"); LimeLog.info("Using Evdev mouse capture");
return new EvdevCaptureProvider(activity, rootListener); return new EvdevCaptureProvider(activity, rootListener);
} }
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) { else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
// Android N's native capture can't capture over system UI elements LimeLog.info("Using Android N+ native mouse capture");
// so we want to only use it if there's no other option. return new AndroidCaptureProvider(activity);
LimeLog.info("Using Android N+ pointer hiding");
return new AndroidPointerIconCaptureProvider(activity);
} }
else { else {
LimeLog.info("Mouse capture not available"); LimeLog.info("Mouse capture not available");
@@ -3,25 +3,10 @@ package com.limelight.binding.input.capture;
import android.view.MotionEvent; import android.view.MotionEvent;
public abstract class InputCaptureProvider { public abstract class InputCaptureProvider {
protected boolean isCapturing; public abstract void enableCapture();
public abstract void disableCapture();
public void enableCapture() {
isCapturing = true;
}
public void disableCapture() {
isCapturing = false;
}
public void destroy() {} public void destroy() {}
public boolean isCapturingEnabled() {
return isCapturing;
}
public boolean isCapturingActive() {
return isCapturing;
}
public boolean eventHasRelativeMouseAxes(MotionEvent event) { public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return false; return false;
} }
@@ -1,4 +1,10 @@
package com.limelight.binding.input.capture; package com.limelight.binding.input.capture;
public class NullCaptureProvider extends InputCaptureProvider {} public class NullCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {}
@Override
public void disableCapture() {}
}
@@ -63,13 +63,11 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
@Override @Override
public void enableCapture() { public void enableCapture() {
super.enableCapture();
setCursorVisibility(false); setCursorVisibility(false);
} }
@Override @Override
public void disableCapture() { public void disableCapture() {
super.disableCapture();
setCursorVisibility(true); setCursorVisibility(true);
} }
@@ -17,22 +17,18 @@ 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,7 +19,6 @@ 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
}; };
@@ -221,7 +221,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
@Override @Override
public void enableCapture() { public void enableCapture() {
super.enableCapture();
if (!started) { if (!started) {
// Start the handler thread if it's our first time // Start the handler thread if it's our first time
// capturing // capturing
@@ -248,7 +247,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
@Override @Override
public void disableCapture() { public void disableCapture() {
super.disableCapture();
// This may be called on the main thread // This may be called on the main thread
runInNetworkSafeContextSynchronously(new Runnable() { runInNetworkSafeContextSynchronously(new Runnable() {
@Override @Override
@@ -1,12 +1,12 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
public interface EvdevListener { public interface EvdevListener {
int BUTTON_LEFT = 1; public static final int BUTTON_LEFT = 1;
int BUTTON_MIDDLE = 2; public static final int BUTTON_MIDDLE = 2;
int BUTTON_RIGHT = 3; public static final int BUTTON_RIGHT = 3;
void mouseMove(int deltaX, int deltaY); public void mouseMove(int deltaX, int deltaY);
void mouseButtonEvent(int buttonId, boolean down); public void mouseButtonEvent(int buttonId, boolean down);
void mouseScroll(byte amount); public void mouseScroll(byte amount);
void keyboardEvent(boolean buttonDown, short keyCode); public void keyboardEvent(boolean buttonDown, short keyCode);
} }
@@ -1,5 +0,0 @@
package com.limelight.binding.video;
public interface CrashListener {
void notifyCrash(Exception e);
}
@@ -12,7 +12,6 @@ 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;
@@ -24,7 +23,17 @@ import android.view.SurfaceHolder;
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private static final boolean USE_FRAME_RENDER_TIME = false; private static final boolean USE_FRAME_RENDER_TIME = false;
private static final boolean FRAME_RENDER_TIME_ONLY = USE_FRAME_RENDER_TIME && false;
// This value is added to the PTS for each frame to force frames delivered right at
// V-sync time to be delayed until the next V-sync rather than dropping the existing frame.
// To make matters worse, these frames often end up being displayed for 2 V-syncs because
// the next frame isn't ready at the next V-sync. So without this, we end up with a pattern of:
// F0 - Displayed | F1 - Dropped | F2 - Displayed | F2 - Displayed again | F3 - Displayed ...
// which can be very noticeable for some video content and users.
//
// This value is currently 6 ms, which means that a frame ready for display less than 6 ms from
// the next V-sync will be delayed until the following V-sync to ensure smooth frame pacing.
private static final int PTS_OFFSET_NS = 6 * 1000000;
// Used on versions < 5.0 // Used on versions < 5.0
private ByteBuffer[] legacyInputBuffers; private ByteBuffer[] legacyInputBuffers;
@@ -32,10 +41,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private MediaCodecInfo avcDecoder; private MediaCodecInfo avcDecoder;
private MediaCodecInfo hevcDecoder; private MediaCodecInfo hevcDecoder;
// Used for HEVC only
private byte[] vpsBuffer;
private byte[] spsBuffer;
private MediaCodec videoDecoder; private MediaCodec videoDecoder;
private Thread rendererThread; private Thread rendererThread;
private Thread[] spinnerThreads; private Thread[] spinnerThreads;
@@ -46,28 +51,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private boolean refFrameInvalidationActive; private boolean refFrameInvalidationActive;
private int initialWidth, initialHeight; private int initialWidth, initialHeight;
private int videoFormat; private int videoFormat;
private SurfaceHolder renderTarget; private Object renderTarget;
private volatile boolean stopping; private 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;
private int totalFrames; private int totalFrames;
private int frameLossEvents;
private int framesLost;
private int lastFrameNumber;
private int refreshRate;
private int bitrate;
private int numSpsIn; private int numSpsIn;
private int numPpsIn; private int numPpsIn;
@@ -109,25 +102,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return decoderInfo; return decoderInfo;
} }
public void setRenderTarget(SurfaceHolder renderTarget) { public void setRenderTarget(Object renderTarget) {
this.renderTarget = renderTarget; this.renderTarget = renderTarget;
} }
public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver, public MediaCodecDecoderRenderer(int videoFormat) {
CrashListener crashListener, int consecutiveCrashCount) {
//dumpDecoders(); //dumpDecoders();
this.bitrate = bitrate; spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
this.crashListener = crashListener;
this.consecutiveCrashCount = consecutiveCrashCount;
// 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) {
@@ -185,7 +167,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
this.initialWidth = width; this.initialWidth = width;
this.initialHeight = height; this.initialHeight = height;
this.videoFormat = format; this.videoFormat = format;
this.refreshRate = redrawRate;
String mimeType; String mimeType;
String selectedDecoderName; String selectedDecoderName;
@@ -263,7 +244,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
try { try {
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0); videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -296,12 +277,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return 0; return 0;
} }
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags, boolean throwOnTransient) { private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) { if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e; CodecException codecExc = (CodecException) e;
if (codecExc.isTransient() && !throwOnTransient) { if (codecExc.isTransient()) {
// We'll let transient exceptions go // We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo()); LimeLog.warning(codecExc.getDiagnosticInfo());
return; return;
@@ -311,35 +292,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
} }
// Only throw if we're not stopping // Only throw if this happens at the beginning of a stream
if (!stopping) { // but not if we're stopping
// if (totalFrames > 0 && totalFrames < 20 && !stopping) {
// There seems to be a race condition with decoder/surface teardown causing some if (buf != null || codecFlags != 0) {
// decoders to to throw IllegalStateExceptions even before 'stopping' is set. throw new RendererException(this, e, buf, codecFlags);
// 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 {
// This is the first exception we've hit throw new RendererException(this, e);
if (buf != null || codecFlags != 0) {
initialException = new RendererException(this, e, buf, codecFlags);
}
else {
initialException = new RendererException(this, e);
}
initialExceptionTimestamp = System.currentTimeMillis();
} }
} }
} }
@@ -350,7 +310,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@Override @Override
public void run() { public void run() {
BufferInfo info = new BufferInfo(); BufferInfo info = new BufferInfo();
while (!stopping) { while (!isInterrupted()) {
try { try {
// Try to output a frame // Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000); int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
@@ -367,7 +327,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
// Render the last buffer // Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// On L and later, we can fine tune our PTS right before rendering, so
// we take advantage of that for frame pacing using the PTS offset. Using
// this version of releaseOutputBuffer() also lets SurfaceFlinger drop extra
// frames in the SurfaceView<->SurfaceFlinger BufferQueue that reduces
// latency if an stray extra frame ends up in the output queue.
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime() + PTS_OFFSET_NS);
}
else {
videoDecoder.releaseOutputBuffer(lastIndex, true);
}
// Add delta time to the totals (excluding probable outliers) // Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000); long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
@@ -390,7 +360,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
} }
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(e, null, 0, false); handleDecoderException(e, null, 0);
} }
} }
} }
@@ -408,7 +378,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 (!stopping) { while (!isInterrupted()) {
try { try {
Thread.sleep(0, 1); Thread.sleep(0, 1);
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -430,11 +400,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
startTime = MediaCodecHelper.getMonotonicMillis(); startTime = MediaCodecHelper.getMonotonicMillis();
try { try {
while (index < 0 && !stopping) { while (rendererThread.isAlive() && index < 0 && !stopping) {
index = videoDecoder.dequeueInputBuffer(10000); index = videoDecoder.dequeueInputBuffer(10000);
} }
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(e, null, 0, true); handleDecoderException(e, null, 0);
return MediaCodec.INFO_TRY_AGAIN_LATER; return MediaCodec.INFO_TRY_AGAIN_LATER;
} }
@@ -457,41 +427,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
startSpinnerThreads(); startSpinnerThreads();
} }
// !!! May be called even if setup()/start() fails !!! @Override
public void prepareForStop() { public void stop() {
// Let the decoding code know to ignore codec exceptions now
stopping = true; stopping = true;
// Halt the rendering thread // Halt the rendering thread
if (rendererThread != null) { rendererThread.interrupt();
rendererThread.interrupt();
}
}
@Override
public void stop() {
// May be called already, but we'll call it now to be safe
prepareForStop();
try {
// Invalidate pending decode buffers
videoDecoder.flush();
} catch (Exception e) {
e.printStackTrace();
}
// Wait for the renderer thread to shut down
try { try {
rendererThread.join(); rendererThread.join();
} catch (InterruptedException ignored) { } } catch (InterruptedException ignored) { }
try {
// Stop the video decoder
videoDecoder.stop();
} catch (Exception e) {
e.printStackTrace();
}
// Halt the spinner threads // Halt the spinner threads
for (Thread t : spinnerThreads) { for (Thread t : spinnerThreads) {
t.interrupt(); t.interrupt();
@@ -509,13 +454,31 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) { private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
try { // Try 25 times to submit the input buffer before throwing a real exception
videoDecoder.queueInputBuffer(inputBufferIndex, int i;
offset, length, Exception lastException = null;
timestampUs, codecFlags);
for (i = 0; i < 25; i++) {
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
offset, length,
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(e, null, codecFlags);
lastException = e;
}
}
if (i == 25 && totalFrames > 0 && totalFrames < 20 && !stopping) {
throw new RendererException(this, lastException, null, codecFlags);
}
else if (i != 25) {
// Queued input buffer
return true; return true;
} catch (Exception e) { }
handleDecoderException(e, null, codecFlags, true); else {
// Failed to queue
return false; return false;
} }
} }
@@ -526,12 +489,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
ByteBuffer buf; ByteBuffer buf;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
try { buf = videoDecoder.getInputBuffer(inputBufferIndex);
buf = videoDecoder.getInputBuffer(inputBufferIndex);
} catch (Exception e) {
handleDecoderException(e, null, 0, true);
return null;
}
} }
else { else {
buf = legacyInputBuffers[inputBufferIndex]; buf = legacyInputBuffers[inputBufferIndex];
@@ -562,36 +520,25 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Override @Override
public int submitDecodeUnit(byte[] frameData, int frameLength, int frameNumber, long receiveTimeMs) { public int submitDecodeUnit(byte[] frameData, int frameLength) {
totalFrames++; totalFrames++;
// 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++;
}
lastFrameNumber = frameNumber;
int inputBufferIndex;
ByteBuffer buf;
long timestampUs = System.nanoTime() / 1000; long timestampUs = System.nanoTime() / 1000;
if (!FRAME_RENDER_TIME_ONLY) {
// Count time from first packet received to decode start
totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
}
if (timestampUs <= lastTimestampUs) { if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp // We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing // so bump it up by one before queuing
timestampUs = lastTimestampUs + 1; timestampUs = lastTimestampUs + 1;
} }
lastTimestampUs = timestampUs; lastTimestampUs = timestampUs;
int inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_OK;
}
ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex);
int codecFlags = 0; int codecFlags = 0;
boolean needsSpsReplay = false; boolean needsSpsReplay = false;
@@ -693,18 +640,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// Patch the SPS constraint flags // Patch the SPS constraint flags
doProfileSpecificSpsPatching(sps); doProfileSpecificSpsPatching(sps);
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// Write the annex B header // Write the annex B header
buf.put(frameData, 0, 5); buf.put(frameData, 0, 5);
@@ -727,17 +662,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
numPpsIn++; numPpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG; codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
if (needsBaselineSpsHack) { if (needsBaselineSpsHack) {
LimeLog.info("Saw PPS; disabling SPS hack"); LimeLog.info("Saw PPS; disabling SPS hack");
@@ -749,66 +673,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
else if (frameData[4] == 0x40) { else if (frameData[4] == 0x40) {
numVpsIn++; numVpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
// Batch this to submit together with SPS and PPS per AOSP docs
vpsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, vpsBuffer, 0, frameLength);
return MoonBridge.DR_OK;
} }
else if (frameData[4] == 0x42) { else if (frameData[4] == 0x42) {
numSpsIn++; numSpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
// Batch this to submit together with VPS and PPS per AOSP docs
spsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, spsBuffer, 0, frameLength);
return MoonBridge.DR_OK;
} }
else if (frameData[4] == 0x44) { else if (frameData[4] == 0x44) {
numPpsIn++; numPpsIn++;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// When we get the PPS, submit the VPS and SPS together with
// the PPS, as required by AOSP docs on use of HEVC and MediaCodec.
if (vpsBuffer != null) {
buf.put(vpsBuffer);
}
if (spsBuffer != null) {
buf.put(spsBuffer);
}
// This is the HEVC CSD blob
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG; codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
} }
else {
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
}
// Copy data from our buffer list into the input buffer // Copy data from our buffer list into the input buffer
buf.put(frameData, 0, frameLength); buf.put(frameData, 0, frameLength);
if (!queueInputBuffer(inputBufferIndex, if (!queueInputBuffer(inputBufferIndex,
0, buf.position(), 0, frameLength,
timestampUs, codecFlags)) { timestampUs, codecFlags)) {
return MoonBridge.DR_NEED_IDR; return MoonBridge.DR_NEED_IDR;
} }
@@ -831,9 +711,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex); ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
if (inputBuffer == null) {
return false;
}
// Write the Annex B header // Write the Annex B header
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67}); inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
@@ -922,14 +799,9 @@ 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 += "Bitrate: "+renderer.bitrate+" Mbps \n";
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n"; str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n"; str += "Total frames: "+renderer.totalFrames+"\n";
str += "Frame losses: "+renderer.framesLost+" in "+frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n"; str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n"; str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
@@ -63,24 +63,15 @@ public class MediaCodecHelper {
static { static {
blacklistedDecoderPrefixes = new LinkedList<>(); blacklistedDecoderPrefixes = new LinkedList<>();
// Blacklist software decoders that don't support H264 high profile, // Software decoders that don't support H264 high profile
// but exclude the official AOSP emulator from this restriction. blacklistedDecoderPrefixes.add("omx.google");
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) { blacklistedDecoderPrefixes.add("AVCDecoder");
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC // Without bitstream fixups, we perform horribly on NVIDIA's HEVC
// decoder. While not strictly necessary, I'm going to fully blacklist this // decoder. While not strictly necessary, I'm going to fully blacklist this
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance. // one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode"); blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
// Force these decoders disabled because:
// 1) They are software decoders, so the performance is terrible
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
} }
static { static {
@@ -107,11 +98,6 @@ public class MediaCodecHelper {
static { static {
whitelistedHevcDecoders = new LinkedList<>(); whitelistedHevcDecoders = new LinkedList<>();
// Allow software HEVC decoding in the official AOSP emulator
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
whitelistedHevcDecoders.add("omx.google");
}
// Exynos seems to be the only HEVC decoder that works reliably // Exynos seems to be the only HEVC decoder that works reliably
whitelistedHevcDecoders.add("omx.exynos"); whitelistedHevcDecoders.add("omx.exynos");
@@ -25,8 +25,6 @@ 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) {
@@ -62,74 +60,50 @@ 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, ADDRESS_PREFIX+details.localAddress); values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress); values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
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 = getComputerFromCursor(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);
}
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.localAddress == null || details.remoteAddress == null || if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) { details.macAddress == null) {
continue; continue;
} }
@@ -145,17 +119,46 @@ 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;
} }
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);
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.localAddress == null || details.remoteAddress == null || if (details.uuid == null || details.localIp == null || details.remoteIp == 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 {
void notifyComputerUpdated(ComputerDetails details); public void notifyComputerUpdated(ComputerDetails details);
} }
@@ -3,6 +3,7 @@ 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;
@@ -19,7 +20,6 @@ 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.localAddress); t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
return t; return t;
} }
@@ -210,8 +210,8 @@ public class ComputerManagerService extends Service {
} }
} }
public boolean addComputerBlocking(String addr, boolean manuallyAdded) { public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded); return ComputerManagerService.this.addComputerBlocking(addr);
} }
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().getHostAddress(), false); addComputerBlocking(computer.getAddress());
} }
@Override @Override
@@ -305,22 +305,15 @@ public class ComputerManagerService extends Service {
}; };
} }
private void addTuple(ComputerDetails details, boolean manuallyAdded) { private void addTuple(ComputerDetails details) {
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)) {
if (manuallyAdded) { // Update details anyway in case this machine has been re-added by IP
// Update details anyway in case this machine has been re-added by IP // after not being reachable by our existing information
// after not being reachable by our existing information tuple.computer.localIp = details.localIp;
tuple.computer.localAddress = details.localAddress; tuple.computer.remoteIp = details.remoteIp;
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) {
@@ -345,11 +338,11 @@ public class ComputerManagerService extends Service {
} }
} }
public boolean addComputerBlocking(String addr, boolean manuallyAdded) { public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder // Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails(); ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localAddress = addr; fakeDetails.localIp = addr;
fakeDetails.remoteAddress = addr; fakeDetails.remoteIp = addr;
// Block while we try to fill the details // Block while we try to fill the details
try { try {
@@ -363,7 +356,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, manuallyAdded); addTuple(fakeDetails);
return true; return true;
} }
else { else {
@@ -411,14 +404,14 @@ public class ComputerManagerService extends Service {
} }
} }
private ComputerDetails tryPollIp(ComputerDetails details, String address) { private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
// 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(address)) { if (!fastPollIp(ipAddr)) {
return null; return null;
} }
try { try {
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails(); ComputerDetails newDetails = http.getComputerDetails();
@@ -439,10 +432,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(String address) { private boolean fastPollIp(InetAddress addr) {
Socket s = new Socket(); Socket s = new Socket();
try { try {
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT); s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
s.close(); s.close();
return true; return true;
} catch (IOException e) { } catch (IOException e) {
@@ -450,11 +443,11 @@ public class ComputerManagerService extends Service {
} }
} }
private void startFastPollThread(final String address, final boolean[] info) { private void startFastPollThread(final InetAddress addr, final boolean[] info) {
Thread t = new Thread() { Thread t = new Thread() {
@Override @Override
public void run() { public void run() {
boolean pollRes = fastPollIp(address); boolean pollRes = fastPollIp(addr);
synchronized (info) { synchronized (info) {
info[0] = true; // Done info[0] = true; // Done
@@ -464,16 +457,16 @@ public class ComputerManagerService extends Service {
} }
} }
}; };
t.setName("Fast Poll - "+address); t.setName("Fast Poll - "+addr.getHostAddress());
t.start(); t.start();
} }
private ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) throws InterruptedException { private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) 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(localAddress, localInfo); startFastPollThread(local, localInfo);
startFastPollThread(remoteAddress, remoteInfo); startFastPollThread(remote, remoteInfo);
// Check local first // Check local first
synchronized (localInfo) { synchronized (localInfo) {
@@ -506,13 +499,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.localAddress.equals(details.remoteAddress)) { if (details.localIp.equals(details.remoteIp)) {
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.localAddress +", "+details.remoteAddress +")"); LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
reachability = fastPollPc(details.localAddress, details.remoteAddress); reachability = fastPollPc(details.localIp, details.remoteIp);
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
@@ -524,39 +517,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.localAddress); polledDetails = tryPollIp(details, details.localIp);
} }
else { else {
polledDetails = tryPollIp(details, details.remoteAddress); polledDetails = tryPollIp(details, details.remoteIp);
} }
String reachableAddr = null; InetAddress reachableAddr = null;
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) { if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback // Failed, so let's try the fallback
if (!localFirst) { if (!localFirst) {
polledDetails = tryPollIp(details, details.localAddress); polledDetails = tryPollIp(details, details.localIp);
} }
else { else {
polledDetails = tryPollIp(details, details.remoteAddress); polledDetails = tryPollIp(details, details.remoteIp);
} }
if (polledDetails != null) { if (polledDetails != null) {
// The fallback poll worked // The fallback poll worked
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress; reachableAddr = !localFirst ? details.localIp : details.remoteIp;
} }
} }
else if (polledDetails != null) { else if (polledDetails != null) {
reachableAddr = localFirst ? details.localAddress : details.remoteAddress; reachableAddr = localFirst ? details.localIp : details.remoteIp;
} }
if (reachableAddr == null) { if (reachableAddr == null) {
return null; return null;
} }
if (polledDetails.remoteAddress.equals(reachableAddr)) { if (polledDetails.remoteIp.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.REMOTE; polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
} }
else if (polledDetails.localAddress.equals(reachableAddr)) { else if (polledDetails.localIp.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.LOCAL; polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
} }
else { else {
@@ -578,7 +571,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.localAddress = initialReachTuple.reachableAddress; initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL; initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
} }
else { else {
@@ -588,11 +581,8 @@ public class ComputerManagerService extends Service {
} }
} }
// Save some details about the old state of the PC that we may wish // Save the old MAC address
// 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);
@@ -603,33 +593,6 @@ 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;
} }
@@ -653,7 +616,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, true); addTuple(computer);
} }
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
@@ -731,6 +694,8 @@ 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) {
@@ -744,12 +709,19 @@ 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
@@ -844,10 +816,10 @@ class PollingTuple {
} }
class ReachabilityTuple { class ReachabilityTuple {
public final String reachableAddress; public final InetAddress reachableAddress;
public final ComputerDetails computer; public final ComputerDetails computer;
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) { public ReachabilityTuple(ComputerDetails computer, InetAddress 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 = convertView.findViewById(R.id.grid_image); ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
ImageView overlayView = convertView.findViewById(R.id.grid_overlay); ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = convertView.findViewById(R.id.grid_text); TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner); ProgressBar prgView = (ProgressBar) 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,11 +4,12 @@ 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;
@@ -20,9 +21,10 @@ 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) {}
@@ -35,4 +37,13 @@ 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,5 +1,8 @@
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;
@@ -14,6 +17,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.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;
@@ -46,12 +50,18 @@ 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);
if (!managerBinder.addComputerBlocking(host, true)){ try {
msg = getResources().getString(R.string.addpc_fail); InetAddress addr = InetAddress.getByName(host);
}
else { if (!managerBinder.addComputerBlocking(addr)){
msg = getResources().getString(R.string.addpc_success); msg = getResources().getString(R.string.addpc_fail);
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();
@@ -132,7 +142,7 @@ public class AddComputerManually extends Activity {
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
this.hostText = findViewById(R.id.hostTextView); this.hostText = (TextView) 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,6 +3,7 @@ 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;
@@ -22,7 +23,6 @@ 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,7 +45,6 @@ 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;
@@ -59,7 +58,6 @@ 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")) {
@@ -130,16 +128,6 @@ 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();
@@ -200,7 +188,6 @@ 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,6 +2,7 @@ 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;
@@ -14,6 +15,8 @@ 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 {
int getAdapterFragmentLayoutId(); public int getAdapterFragmentLayoutId();
void receiveAbsListView(AbsListView gridView); public void receiveAbsListView(AbsListView gridView);
} }
@@ -1,5 +1,5 @@
package com.limelight.ui; package com.limelight.ui;
public interface GameGestures { public interface GameGestures {
void showKeyboard(); public void showKeyboard();
} }
@@ -1,5 +1,6 @@
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,18 +14,21 @@ 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 String getCurrentAddressFromComputer(ComputerDetails computer) { public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ? return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localAddress : computer.remoteAddress; computer.localIp : computer.remoteIp;
} }
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, getCurrentAddressFromComputer(computer)); intent.putExtra(Game.EXTRA_HOST,
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());
@@ -42,7 +45,7 @@ public class ServerHelper {
} }
public static void doQuit(final Activity parent, public static void doQuit(final Activity parent,
final String address, final InetAddress 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.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 65 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=".preferences.AddComputerManually" > tools:context=".AddComputerManually" >
<TextView <TextView
android:id="@+id/manuallyAddPcText" android:id="@+id/manuallyAddPcText"
+19
View File
@@ -0,0 +1,19 @@
<?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=".preferences.StreamSettings"> tools:context=".StreamSettings">
</RelativeLayout> </RelativeLayout>
@@ -1,5 +0,0 @@
<?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>
@@ -1,5 +0,0 @@
<?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.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#565C64</color>
</resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_pc_scut_background">#FFFFFF</color>
</resources>
-6
View File
@@ -47,10 +47,6 @@
<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>
@@ -108,8 +104,6 @@
<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_battery_saver" android:key="checkbox_disable_warnings"
android:title="@string/title_checkbox_battery_saver" android:title="@string/title_checkbox_disable_warnings"
android:summary="@string/summary_checkbox_battery_saver" android:summary="@string/summary_checkbox_disable_warnings"
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:3.0.0-beta5' classpath 'com.android.tools.build:gradle:2.3.2'
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
#Sun Sep 03 12:51:12 PDT 2017 #Thu Mar 02 18:32:01 PST 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-4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-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" 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" external.system.module.group="" external.system.module.version="unspecified" 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.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

-92
View File
@@ -1,92 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.4 KiB