Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9f1142af7 | |||
| 38a6a2b74a | |||
| fd2421618a | |||
| 79a9ea7179 | |||
| 34a11c9262 | |||
| 84a9845c1d | |||
| 5b05220008 | |||
| b2bd7257e1 | |||
| 46a998c113 | |||
| 60cd951774 | |||
| d4f8d8f689 | |||
| 608a0ebb5b | |||
| f01a15d182 | |||
| 0268b4f958 | |||
| d71cf0eb98 | |||
| 10ab40f823 | |||
| 427edfa021 | |||
| 6f18831d5c | |||
| a3db09f422 | |||
| d185a05b1d | |||
| 78e575504a | |||
| 0a0be19b69 | |||
| 0792157e9d | |||
| cdd0ecf0b7 | |||
| 1ac721a35b | |||
| e49b1c92a2 | |||
| db4295bf83 | |||
| 824c37f9d5 | |||
| acf4426952 | |||
| e8c50342ab | |||
| 598995de3b | |||
| 01cf0cc649 | |||
| fa560f462f | |||
| f6e40118a9 | |||
| fe7148dbd4 | |||
| 60de065836 | |||
| 6f82f82abb |
@@ -1,4 +1,4 @@
|
|||||||
#Moonlight
|
# Moonlight
|
||||||
|
|
||||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
@@ -10,35 +10,35 @@ whether in your own home or over the internet.
|
|||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
##Features
|
## Features
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
* Streams any of your games from your PC to your Android device
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* Automatically finds GameStream-compatible PCs on your network
|
||||||
|
|
||||||
##Installation
|
## Installation
|
||||||
|
|
||||||
* Download and install Moonlight for Android from
|
* Download and install Moonlight for Android from
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||||
|
|
||||||
##Requirements
|
## Requirements
|
||||||
|
|
||||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
|
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
||||||
* Android device running 4.1 (Jelly Bean) or higher
|
* Android device running 4.1 (Jelly Bean) or higher
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
* High-end wireless router (802.11n dual-band recommended)
|
||||||
|
|
||||||
##Usage
|
## Usage
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC
|
* Accept the pairing confirmation on your PC and add the PIN if needed
|
||||||
* Tap your PC again to view the list of apps to stream
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
|
|
||||||
##Contribute
|
## Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
|
|
||||||
@@ -46,13 +46,13 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
##Building
|
## Building
|
||||||
* Install Android Studio and the Android NDK
|
* Install Android Studio and the Android NDK
|
||||||
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
||||||
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
||||||
* Build the APK using Android Studio
|
* Build the APK using Android Studio
|
||||||
|
|
||||||
##Authors
|
## Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ apply plugin: 'com.android.application'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 26
|
compileSdkVersion 26
|
||||||
buildToolsVersion '26.0.0'
|
buildToolsVersion '26.0.1'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 26
|
targetSdkVersion 26
|
||||||
|
|
||||||
versionName "5.1"
|
versionName "5.2"
|
||||||
versionCode = 128
|
versionCode = 131
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "root"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
root {
|
root {
|
||||||
// Android O has native mouse capture, so don't show the rooted
|
// Android O has native mouse capture, so don't show the rooted
|
||||||
@@ -25,6 +27,8 @@ android {
|
|||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
@@ -32,6 +36,8 @@ android {
|
|||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +60,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.57'
|
||||||
compile 'org.bouncycastle:bcpkix-jdk15on:1.52'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.57'
|
||||||
compile files('libs/jcodec-0.1.9-patched.jar')
|
implementation files('libs/jcodec-0.1.9-patched.jar')
|
||||||
|
|
||||||
debugCompile project(path:':moonlight-common', configuration:'debug')
|
implementation project(':moonlight-common')
|
||||||
releaseCompile project(path:':moonlight-common', configuration:'release')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,19 @@
|
|||||||
android:name="android.software.leanback"
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<!-- Disable legacy input emulation on ChromeOS -->
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.type.pc"
|
||||||
|
android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:isGame="true"
|
android:isGame="true"
|
||||||
android:banner="@drawable/atv_banner"
|
android:banner="@drawable/atv_banner"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:appCategory="game"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<!-- Samsung multi-window support -->
|
<!-- Samsung multi-window support -->
|
||||||
|
|||||||
|
After Width: | Height: | Size: 58 KiB |
@@ -2,7 +2,6 @@ package com.limelight;
|
|||||||
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
@@ -27,7 +26,6 @@ import android.app.Service;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
@@ -250,7 +248,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
|
||||||
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
||||||
TextView label = (TextView) findViewById(R.id.appListText);
|
TextView label = findViewById(R.id.appListText);
|
||||||
setTitle(labelText);
|
setTitle(labelText);
|
||||||
label.setText(labelText);
|
label.setText(labelText);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.limelight.binding.input.TouchContext;
|
|||||||
import com.limelight.binding.input.driver.UsbDriverService;
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
import com.limelight.binding.input.virtual_controller.VirtualController;
|
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||||
|
import com.limelight.binding.video.CrashListener;
|
||||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
@@ -34,6 +35,7 @@ import android.content.ComponentName;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
@@ -81,6 +83,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private VirtualController virtualController;
|
private VirtualController virtualController;
|
||||||
|
|
||||||
private PreferenceConfiguration prefConfig;
|
private PreferenceConfiguration prefConfig;
|
||||||
|
private SharedPreferences tombstonePrefs;
|
||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
private SpinnerDialog spinner;
|
private SpinnerDialog spinner;
|
||||||
@@ -165,9 +168,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Read the stream preferences
|
// Read the stream preferences
|
||||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
|
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
|
||||||
|
|
||||||
// Listen for events on the game surface
|
// Listen for events on the game surface
|
||||||
streamView = (StreamView) findViewById(R.id.surfaceView);
|
streamView = findViewById(R.id.surfaceView);
|
||||||
streamView.setOnGenericMotionListener(this);
|
streamView.setOnGenericMotionListener(this);
|
||||||
streamView.setOnTouchListener(this);
|
streamView.setOnTouchListener(this);
|
||||||
|
|
||||||
@@ -214,7 +219,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Initialize the MediaCodec helper before creating the decoder
|
// Initialize the MediaCodec helper before creating the decoder
|
||||||
MediaCodecHelper.initializeWithContext(this);
|
MediaCodecHelper.initializeWithContext(this);
|
||||||
|
|
||||||
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat, prefConfig.bitrate);
|
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat,
|
||||||
|
prefConfig.bitrate,
|
||||||
|
prefConfig.batterySaver,
|
||||||
|
new CrashListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyCrash(Exception e) {
|
||||||
|
// The MediaCodec instance is going down due to a crash
|
||||||
|
// let's tell the user something when they open the app again
|
||||||
|
|
||||||
|
// We must use commit because the app will crash when we return from this function
|
||||||
|
tombstonePrefs.edit().putInt("CrashCount", tombstonePrefs.getInt("CrashCount", 0) + 1).commit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tombstonePrefs.getInt("CrashCount", 0));
|
||||||
|
|
||||||
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
|
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
|
||||||
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
||||||
@@ -274,7 +292,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (prefConfig.onscreenController) {
|
if (prefConfig.onscreenController) {
|
||||||
// create virtual onscreen controller
|
// create virtual onscreen controller
|
||||||
virtualController = new VirtualController(conn,
|
virtualController = new VirtualController(conn,
|
||||||
(FrameLayout)findViewById(R.id.surfaceView).getParent(),
|
(FrameLayout)streamView.getParent(),
|
||||||
this);
|
this);
|
||||||
virtualController.refreshLayout();
|
virtualController.refreshLayout();
|
||||||
}
|
}
|
||||||
@@ -489,6 +507,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (message != null) {
|
if (message != null) {
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the tombstone count
|
||||||
|
if (tombstonePrefs.getInt("CrashCount", 0) != 0) {
|
||||||
|
tombstonePrefs.edit().putInt("CrashCount", 0).apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
@@ -596,9 +619,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
boolean handled = false;
|
boolean handled = false;
|
||||||
|
|
||||||
boolean detectedGamepad = event.getDevice() == null ? false :
|
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
||||||
((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
|
||||||
if (detectedGamepad || (event.getDevice() == null ||
|
if (detectedGamepad || (event.getDevice() == null ||
|
||||||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
||||||
)) {
|
)) {
|
||||||
@@ -638,9 +660,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean handled = false;
|
boolean handled = false;
|
||||||
boolean detectedGamepad = event.getDevice() == null ? false :
|
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
||||||
((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
|
||||||
if (detectedGamepad || (event.getDevice() == null ||
|
if (detectedGamepad || (event.getDevice() == null ||
|
||||||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
||||||
)) {
|
)) {
|
||||||
@@ -944,6 +965,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
LimeLog.severe(stage+" failed: "+errorCode);
|
LimeLog.severe(stage+" failed: "+errorCode);
|
||||||
|
|
||||||
|
// If video initialization failed and the surface is still valid, display extra information for the user
|
||||||
|
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||||
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,14 +51,8 @@ public class HelpActivity extends Activity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
if (url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
||||||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase())) {
|
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
|
||||||
// Allow navigation to Moonlight docs
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package com.limelight;
|
|||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
@@ -33,6 +31,7 @@ import android.app.Service;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
@@ -110,9 +109,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
||||||
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
||||||
ImageButton helpButton = (ImageButton) findViewById(R.id.helpButton);
|
ImageButton helpButton = findViewById(R.id.helpButton);
|
||||||
|
|
||||||
settingsButton.setOnClickListener(new OnClickListener() {
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -138,7 +137,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
|
|
||||||
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
|
noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
|
||||||
if (pcGridAdapter.getCount() == 0) {
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
@@ -165,6 +164,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
||||||
|
|
||||||
initializeViews();
|
initializeViews();
|
||||||
|
|
||||||
|
SharedPreferences prefs = getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
int crashCount = prefs.getInt("CrashCount", 0);
|
||||||
|
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
|
||||||
|
|
||||||
|
// Remember the last crash count we notified at, so we don't
|
||||||
|
// display the crash dialog every time the app is started until
|
||||||
|
// they stream again
|
||||||
|
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
|
||||||
|
if (crashCount % 3 == 0) {
|
||||||
|
// At 3 consecutive crashes, we'll forcefully reset their settings
|
||||||
|
PreferenceConfiguration.resetStreamingSettings(this);
|
||||||
|
Dialog.displayDialog(this,
|
||||||
|
getResources().getString(R.string.title_decoding_reset),
|
||||||
|
getResources().getString(R.string.message_decoding_reset),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Dialog.displayDialog(this,
|
||||||
|
getResources().getString(R.string.title_decoding_error),
|
||||||
|
getResources().getString(R.string.message_decoding_error),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
@@ -306,19 +331,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Stop updates and wait while pairing
|
// Stop updates and wait while pairing
|
||||||
stopComputerUpdates(true);
|
stopComputerUpdates(true);
|
||||||
|
|
||||||
InetAddress addr;
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
|
||||||
addr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.warning("Unknown reachability - using local IP");
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
PlatformBinding.getDeviceName(),
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
@@ -443,19 +456,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
InetAddress addr;
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
|
||||||
addr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.warning("Unknown reachability - using local IP");
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
PlatformBinding.getDeviceName(),
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.limelight.binding.audio;
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
|
import android.media.AudioAttributes;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
@@ -12,10 +14,58 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
|
|
||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
|
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
|
48000,
|
||||||
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_GAME);
|
||||||
|
AudioFormat format = new AudioFormat.Builder()
|
||||||
|
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||||
|
.setSampleRate(48000)
|
||||||
|
.setChannelMask(channelConfig)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
// Use FLAG_LOW_LATENCY on L through N
|
||||||
|
if (lowLatency) {
|
||||||
|
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
|
||||||
|
.setAudioFormat(format)
|
||||||
|
.setAudioAttributes(attributesBuilder.build())
|
||||||
|
.setTransferMode(AudioTrack.MODE_STREAM)
|
||||||
|
.setBufferSizeInBytes(bufferSize);
|
||||||
|
|
||||||
|
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
|
||||||
|
if (lowLatency) {
|
||||||
|
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackBuilder.build();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new AudioTrack(attributesBuilder.build(),
|
||||||
|
format,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM,
|
||||||
|
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int setup(int audioConfiguration) {
|
public int setup(int audioConfiguration) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bufferSize;
|
|
||||||
int bytesPerFrame;
|
int bytesPerFrame;
|
||||||
|
|
||||||
switch (audioConfiguration)
|
switch (audioConfiguration)
|
||||||
@@ -38,44 +88,82 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
// do this on many devices and it lowers audio latency.
|
// do this on many devices and it lowers audio latency.
|
||||||
// We'll try the small buffer size first and if it fails,
|
// We'll try the small buffer size first and if it fails,
|
||||||
// use the recommended larger buffer size.
|
// use the recommended larger buffer size.
|
||||||
try {
|
|
||||||
// Buffer two frames of audio if possible
|
|
||||||
bufferSize = bytesPerFrame * 2;
|
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
for (int i = 0; i < 4; i++) {
|
||||||
48000,
|
boolean lowLatency;
|
||||||
channelConfig,
|
int bufferSize;
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
|
||||||
track.play();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Try to release the AudioTrack if we got far enough
|
|
||||||
try {
|
|
||||||
if (track != null) {
|
|
||||||
track.release();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
// Now try the larger buffer size
|
// We will try:
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
// 1) Small buffer, low latency mode
|
||||||
|
// 2) Large buffer, low latency mode
|
||||||
|
// 3) Small buffer, standard mode
|
||||||
|
// 4) Large buffer, standard mode
|
||||||
|
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
lowLatency = true;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
lowLatency = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unreachable
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
case 2:
|
||||||
|
bufferSize = bytesPerFrame * 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
case 3:
|
||||||
|
// Try the larger buffer size
|
||||||
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
bytesPerFrame * 2);
|
bytesPerFrame * 2);
|
||||||
|
|
||||||
// Round to next frame
|
// Round to next frame
|
||||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unreachable
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
// Skip low latency options if hardware sample rate isn't 48000Hz
|
||||||
48000,
|
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
||||||
channelConfig,
|
continue;
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
}
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
try {
|
||||||
track.play();
|
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
||||||
|
track.play();
|
||||||
|
|
||||||
|
// Successfully created working AudioTrack. We're done here.
|
||||||
|
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Try to release the AudioTrack if we got far enough
|
||||||
|
e.printStackTrace();
|
||||||
|
try {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
track = null;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track == null) {
|
||||||
|
// Couldn't create any audio track for playback
|
||||||
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Audio track buffer size: "+bufferSize);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to translate a Android key code into the codes GFE is expecting
|
* Class to translate a Android key code into the codes GFE is expecting
|
||||||
* @author Diego Waxemberg
|
* @author Diego Waxemberg
|
||||||
|
|||||||
@@ -17,18 +17,22 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
0x044f, // Thrustmaster
|
0x044f, // Thrustmaster
|
||||||
0x045e, // Microsoft
|
0x045e, // Microsoft
|
||||||
0x046d, // Logitech
|
0x046d, // Logitech
|
||||||
|
0x056e, // Elecom
|
||||||
|
0x06a3, // Saitek
|
||||||
0x0738, // Mad Catz
|
0x0738, // Mad Catz
|
||||||
|
0x07ff, // Mad Catz
|
||||||
0x0e6f, // Unknown
|
0x0e6f, // Unknown
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x11c9, // Nacon
|
||||||
0x12ab, // Unknown
|
0x12ab, // Unknown
|
||||||
0x1430, // RedOctane
|
0x1430, // RedOctane
|
||||||
0x146b, // BigBen
|
0x146b, // BigBen
|
||||||
0x1bad, // Harmonix
|
|
||||||
0x0f0d, // Hori
|
|
||||||
0x1689, // Razer Onza
|
|
||||||
0x24c6, // PowerA
|
|
||||||
0x1532, // Razer Sabertooth
|
0x1532, // Razer Sabertooth
|
||||||
0x15e4, // Numark
|
0x15e4, // Numark
|
||||||
0x162e, // Joytech
|
0x162e, // Joytech
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x24c6, // PowerA
|
||||||
};
|
};
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
0x0738, // Mad Catz
|
0x0738, // Mad Catz
|
||||||
0x0e6f, // Unknown
|
0x0e6f, // Unknown
|
||||||
0x0f0d, // Hori
|
0x0f0d, // Hori
|
||||||
|
0x1532, // Razer Wildcat
|
||||||
0x24c6, // PowerA
|
0x24c6, // PowerA
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
public interface EvdevListener {
|
public interface EvdevListener {
|
||||||
public static final int BUTTON_LEFT = 1;
|
int BUTTON_LEFT = 1;
|
||||||
public static final int BUTTON_MIDDLE = 2;
|
int BUTTON_MIDDLE = 2;
|
||||||
public static final int BUTTON_RIGHT = 3;
|
int BUTTON_RIGHT = 3;
|
||||||
|
|
||||||
public void mouseMove(int deltaX, int deltaY);
|
void mouseMove(int deltaX, int deltaY);
|
||||||
public void mouseButtonEvent(int buttonId, boolean down);
|
void mouseButtonEvent(int buttonId, boolean down);
|
||||||
public void mouseScroll(byte amount);
|
void mouseScroll(byte amount);
|
||||||
public void keyboardEvent(boolean buttonDown, short keyCode);
|
void keyboardEvent(boolean buttonDown, short keyCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
public interface CrashListener {
|
||||||
|
void notifyCrash(Exception e);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
|||||||
import com.limelight.nvstream.jni.MoonBridge;
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
@@ -47,10 +48,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private int videoFormat;
|
private int videoFormat;
|
||||||
private SurfaceHolder renderTarget;
|
private SurfaceHolder renderTarget;
|
||||||
private volatile boolean stopping;
|
private volatile boolean stopping;
|
||||||
|
private CrashListener crashListener;
|
||||||
|
private boolean reportedCrash;
|
||||||
|
private int consecutiveCrashCount;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
private SeqParameterSet savedSps;
|
private SeqParameterSet savedSps;
|
||||||
|
|
||||||
|
private RendererException initialException;
|
||||||
|
private long initialExceptionTimestamp;
|
||||||
|
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
|
||||||
|
|
||||||
private long lastTimestampUs;
|
private long lastTimestampUs;
|
||||||
private long decoderTimeMs;
|
private long decoderTimeMs;
|
||||||
private long totalTimeMs;
|
private long totalTimeMs;
|
||||||
@@ -105,12 +113,21 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
this.renderTarget = renderTarget;
|
this.renderTarget = renderTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaCodecDecoderRenderer(int videoFormat, int bitrate) {
|
public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver,
|
||||||
|
CrashListener crashListener, int consecutiveCrashCount) {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
|
|
||||||
this.bitrate = bitrate;
|
this.bitrate = bitrate;
|
||||||
|
this.crashListener = crashListener;
|
||||||
|
this.consecutiveCrashCount = consecutiveCrashCount;
|
||||||
|
|
||||||
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
|
// Disable spinner threads in battery saver mode
|
||||||
|
if (batterySaver) {
|
||||||
|
spinnerThreads = new Thread[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
|
||||||
|
}
|
||||||
|
|
||||||
avcDecoder = findAvcDecoder();
|
avcDecoder = findAvcDecoder();
|
||||||
if (avcDecoder != null) {
|
if (avcDecoder != null) {
|
||||||
@@ -294,13 +311,35 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only throw if the surface is still around and we're not stopping
|
// Only throw if we're not stopping
|
||||||
if (!stopping && renderTarget.getSurface().isValid()) {
|
if (!stopping) {
|
||||||
if (buf != null || codecFlags != 0) {
|
//
|
||||||
throw new RendererException(this, e, buf, codecFlags);
|
// There seems to be a race condition with decoder/surface teardown causing some
|
||||||
|
// decoders to to throw IllegalStateExceptions even before 'stopping' is set.
|
||||||
|
// To workaround this while allowing real exceptions to propagate, we will eat the
|
||||||
|
// first exception. If we are still receiving exceptions 3 seconds later, we will
|
||||||
|
// throw the original exception again.
|
||||||
|
//
|
||||||
|
if (initialException != null) {
|
||||||
|
// This isn't the first time we've had an exception processing video
|
||||||
|
if (System.currentTimeMillis() - initialExceptionTimestamp >= EXCEPTION_REPORT_DELAY_MS) {
|
||||||
|
// It's been over 3 seconds and we're still getting exceptions. Throw the original now.
|
||||||
|
if (!reportedCrash) {
|
||||||
|
reportedCrash = true;
|
||||||
|
crashListener.notifyCrash(initialException);
|
||||||
|
}
|
||||||
|
throw initialException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new RendererException(this, e);
|
// This is the first exception we've hit
|
||||||
|
if (buf != null || codecFlags != 0) {
|
||||||
|
initialException = new RendererException(this, e, buf, codecFlags);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
initialException = new RendererException(this, e);
|
||||||
|
}
|
||||||
|
initialExceptionTimestamp = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +350,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
while (!isInterrupted()) {
|
while (!stopping) {
|
||||||
try {
|
try {
|
||||||
// Try to output a frame
|
// Try to output a frame
|
||||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
||||||
@@ -369,7 +408,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
public void run() {
|
public void run() {
|
||||||
// This thread exists to keep the CPU at a higher DVFS state on devices
|
// This thread exists to keep the CPU at a higher DVFS state on devices
|
||||||
// where the governor scales clock speed sporadically, causing dropped frames.
|
// where the governor scales clock speed sporadically, causing dropped frames.
|
||||||
while (!isInterrupted()) {
|
while (!stopping) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(0, 1);
|
Thread.sleep(0, 1);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@@ -418,17 +457,21 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
startSpinnerThreads();
|
startSpinnerThreads();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// !!! May be called even if setup()/start() fails !!!
|
||||||
public void prepareForStop() {
|
public void prepareForStop() {
|
||||||
// Let the decoding code know to ignore codec exceptions now
|
// Let the decoding code know to ignore codec exceptions now
|
||||||
stopping = true;
|
stopping = true;
|
||||||
|
|
||||||
|
// Halt the rendering thread
|
||||||
|
if (rendererThread != null) {
|
||||||
|
rendererThread.interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
stopping = true;
|
// May be called already, but we'll call it now to be safe
|
||||||
|
prepareForStop();
|
||||||
// Halt the rendering thread
|
|
||||||
rendererThread.interrupt();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Invalidate pending decode buffers
|
// Invalidate pending decode buffers
|
||||||
@@ -653,13 +696,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
inputBufferIndex = dequeueInputBuffer();
|
inputBufferIndex = dequeueInputBuffer();
|
||||||
if (inputBufferIndex < 0) {
|
if (inputBufferIndex < 0) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||||
if (buf == null) {
|
if (buf == null) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the annex B header
|
// Write the annex B header
|
||||||
@@ -687,13 +730,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
inputBufferIndex = dequeueInputBuffer();
|
inputBufferIndex = dequeueInputBuffer();
|
||||||
if (inputBufferIndex < 0) {
|
if (inputBufferIndex < 0) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||||
if (buf == null) {
|
if (buf == null) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsBaselineSpsHack) {
|
if (needsBaselineSpsHack) {
|
||||||
@@ -726,13 +769,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
inputBufferIndex = dequeueInputBuffer();
|
inputBufferIndex = dequeueInputBuffer();
|
||||||
if (inputBufferIndex < 0) {
|
if (inputBufferIndex < 0) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||||
if (buf == null) {
|
if (buf == null) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When we get the PPS, submit the VPS and SPS together with
|
// When we get the PPS, submit the VPS and SPS together with
|
||||||
@@ -751,13 +794,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
inputBufferIndex = dequeueInputBuffer();
|
inputBufferIndex = dequeueInputBuffer();
|
||||||
if (inputBufferIndex < 0) {
|
if (inputBufferIndex < 0) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||||
if (buf == null) {
|
if (buf == null) {
|
||||||
// We're being torn down now
|
// We're being torn down now
|
||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,6 +922,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
str += "Format: "+renderer.videoFormat+"\n";
|
str += "Format: "+renderer.videoFormat+"\n";
|
||||||
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
||||||
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
||||||
|
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
||||||
|
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
||||||
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||||
str += "FPS target: "+renderer.refreshRate+"\n";
|
str += "FPS target: "+renderer.refreshRate+"\n";
|
||||||
str += "Bitrate: "+renderer.bitrate+" Mbps \n";
|
str += "Bitrate: "+renderer.bitrate+" Mbps \n";
|
||||||
@@ -900,6 +945,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
|
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
|
||||||
|
|
||||||
|
str += "/proc/cpuinfo:\n";
|
||||||
|
try {
|
||||||
|
str += MediaCodecHelper.readCpuinfo();
|
||||||
|
} catch (Exception e) {
|
||||||
|
str += e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
str += "Full decoder dump:\n";
|
str += "Full decoder dump:\n";
|
||||||
try {
|
try {
|
||||||
str += MediaCodecHelper.dumpDecoders();
|
str += MediaCodecHelper.dumpDecoders();
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public class ComputerDatabaseManager {
|
|||||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||||
private static final String MAC_COLUMN_NAME = "Mac";
|
private static final String MAC_COLUMN_NAME = "Mac";
|
||||||
|
|
||||||
|
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
public ComputerDatabaseManager(Context c) {
|
public ComputerDatabaseManager(Context c) {
|
||||||
@@ -60,50 +62,74 @@ public class ComputerDatabaseManager {
|
|||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||||
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
|
||||||
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
|
||||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
|
details.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An earlier schema defined addresses as byte blobs. We'll
|
||||||
|
// gracefully migrate those to strings so we can store DNS names
|
||||||
|
// too. To disambiguate, we'll need to prefix them with a string
|
||||||
|
// greater than the allowable IP address length.
|
||||||
|
try {
|
||||||
|
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
||||||
|
LimeLog.warning("DB: Legacy local address for "+details.name);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// This is probably a hostname/address with the prefix string
|
||||||
|
String stringData = c.getString(2);
|
||||||
|
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||||
|
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
||||||
|
LimeLog.warning("DB: Legacy remote address for "+details.name);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// This is probably a hostname/address with the prefix string
|
||||||
|
String stringData = c.getString(3);
|
||||||
|
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||||
|
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
public List<ComputerDetails> getAllComputers() {
|
public List<ComputerDetails> getAllComputers() {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
|
|
||||||
details.name = c.getString(0);
|
|
||||||
|
|
||||||
String uuidStr = c.getString(1);
|
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
|
|
||||||
// If a field is corrupt or missing, skip the database entry
|
// If a field is corrupt or missing, skip the database entry
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
||||||
details.macAddress == null) {
|
details.macAddress == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -119,46 +145,17 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
public ComputerDetails getComputerByName(String name) {
|
public ComputerDetails getComputerByName(String name) {
|
||||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
if (!c.moveToFirst()) {
|
if (!c.moveToFirst()) {
|
||||||
// No matching computer
|
// No matching computer
|
||||||
c.close();
|
c.close();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
details.name = c.getString(0);
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
|
|
||||||
String uuidStr = c.getString(1);
|
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
|
|
||||||
// If a field is corrupt or missing, delete the database entry
|
// If a field is corrupt or missing, delete the database entry
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
||||||
details.macAddress == null) {
|
details.macAddress == null) {
|
||||||
deleteComputer(details.name);
|
deleteComputer(details.name);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package com.limelight.computers;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
public interface ComputerManagerListener {
|
public interface ComputerManagerListener {
|
||||||
public void notifyComputerUpdated(ComputerDetails details);
|
void notifyComputerUpdated(ComputerDetails details);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.limelight.computers;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -20,6 +19,7 @@ import com.limelight.nvstream.http.NvHTTP;
|
|||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -154,7 +154,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
|
t.setName("Polling thread for " + tuple.computer.localAddress);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,8 +210,8 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(InetAddress addr) {
|
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(String name) {
|
||||||
@@ -289,7 +289,7 @@ public class ComputerManagerService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
// Kick off a serverinfo poll on this machine
|
// Kick off a serverinfo poll on this machine
|
||||||
addComputerBlocking(computer.getAddress());
|
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -305,15 +305,22 @@ public class ComputerManagerService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTuple(ComputerDetails details) {
|
private void addTuple(ComputerDetails details, boolean manuallyAdded) {
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
// Check if this is the same computer
|
// Check if this is the same computer
|
||||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||||
// Update details anyway in case this machine has been re-added by IP
|
if (manuallyAdded) {
|
||||||
// after not being reachable by our existing information
|
// Update details anyway in case this machine has been re-added by IP
|
||||||
tuple.computer.localIp = details.localIp;
|
// after not being reachable by our existing information
|
||||||
tuple.computer.remoteIp = details.remoteIp;
|
tuple.computer.localAddress = details.localAddress;
|
||||||
|
tuple.computer.remoteAddress = details.remoteAddress;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This indicates that mDNS discovered this address, so we
|
||||||
|
// should only apply the local address.
|
||||||
|
tuple.computer.localAddress = details.localAddress;
|
||||||
|
}
|
||||||
|
|
||||||
// Start a polling thread if polling is active
|
// Start a polling thread if polling is active
|
||||||
if (pollingActive && tuple.thread == null) {
|
if (pollingActive && tuple.thread == null) {
|
||||||
@@ -338,11 +345,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(InetAddress addr) {
|
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||||
// Setup a placeholder
|
// Setup a placeholder
|
||||||
ComputerDetails fakeDetails = new ComputerDetails();
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
fakeDetails.localIp = addr;
|
fakeDetails.localAddress = addr;
|
||||||
fakeDetails.remoteIp = addr;
|
fakeDetails.remoteAddress = addr;
|
||||||
|
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
try {
|
try {
|
||||||
@@ -356,7 +363,7 @@ public class ComputerManagerService extends Service {
|
|||||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||||
|
|
||||||
// Start a polling thread for this machine
|
// Start a polling thread for this machine
|
||||||
addTuple(fakeDetails);
|
addTuple(fakeDetails, manuallyAdded);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -404,14 +411,14 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
|
||||||
// Fast poll this address first to determine if we can connect at the TCP layer
|
// Fast poll this address first to determine if we can connect at the TCP layer
|
||||||
if (!fastPollIp(ipAddr)) {
|
if (!fastPollIp(address)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
ComputerDetails newDetails = http.getComputerDetails();
|
ComputerDetails newDetails = http.getComputerDetails();
|
||||||
@@ -432,10 +439,10 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
// Just try to establish a TCP connection to speculatively detect a running
|
// Just try to establish a TCP connection to speculatively detect a running
|
||||||
// GFE server
|
// GFE server
|
||||||
private boolean fastPollIp(InetAddress addr) {
|
private boolean fastPollIp(String address) {
|
||||||
Socket s = new Socket();
|
Socket s = new Socket();
|
||||||
try {
|
try {
|
||||||
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||||
s.close();
|
s.close();
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -443,11 +450,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
private void startFastPollThread(final String address, final boolean[] info) {
|
||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean pollRes = fastPollIp(addr);
|
boolean pollRes = fastPollIp(address);
|
||||||
|
|
||||||
synchronized (info) {
|
synchronized (info) {
|
||||||
info[0] = true; // Done
|
info[0] = true; // Done
|
||||||
@@ -457,16 +464,16 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Fast Poll - "+addr.getHostAddress());
|
t.setName("Fast Poll - "+address);
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
private ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) throws InterruptedException {
|
||||||
final boolean[] remoteInfo = new boolean[2];
|
final boolean[] remoteInfo = new boolean[2];
|
||||||
final boolean[] localInfo = new boolean[2];
|
final boolean[] localInfo = new boolean[2];
|
||||||
|
|
||||||
startFastPollThread(local, localInfo);
|
startFastPollThread(localAddress, localInfo);
|
||||||
startFastPollThread(remote, remoteInfo);
|
startFastPollThread(remoteAddress, remoteInfo);
|
||||||
|
|
||||||
// Check local first
|
// Check local first
|
||||||
synchronized (localInfo) {
|
synchronized (localInfo) {
|
||||||
@@ -499,13 +506,13 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
// If the local address is routable across the Internet,
|
// If the local address is routable across the Internet,
|
||||||
// always consider this PC remote to be conservative
|
// always consider this PC remote to be conservative
|
||||||
if (details.localIp.equals(details.remoteIp)) {
|
if (details.localAddress.equals(details.remoteAddress)) {
|
||||||
reachability = ComputerDetails.Reachability.REMOTE;
|
reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +")");
|
||||||
reachability = fastPollPc(details.localIp, details.remoteIp);
|
reachability = fastPollPc(details.localAddress, details.remoteAddress);
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
// If no connection could be established to either IP address, there's nothing we can do
|
||||||
@@ -517,39 +524,39 @@ public class ComputerManagerService extends Service {
|
|||||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
|
||||||
if (localFirst) {
|
if (localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
polledDetails = tryPollIp(details, details.localAddress);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
polledDetails = tryPollIp(details, details.remoteAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
InetAddress reachableAddr = null;
|
String reachableAddr = null;
|
||||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
|
||||||
// Failed, so let's try the fallback
|
// Failed, so let's try the fallback
|
||||||
if (!localFirst) {
|
if (!localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
polledDetails = tryPollIp(details, details.localAddress);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
polledDetails = tryPollIp(details, details.remoteAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (polledDetails != null) {
|
if (polledDetails != null) {
|
||||||
// The fallback poll worked
|
// The fallback poll worked
|
||||||
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
|
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (polledDetails != null) {
|
else if (polledDetails != null) {
|
||||||
reachableAddr = localFirst ? details.localIp : details.remoteIp;
|
reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reachableAddr == null) {
|
if (reachableAddr == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (polledDetails.remoteIp.equals(reachableAddr)) {
|
if (polledDetails.remoteAddress.equals(reachableAddr)) {
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else if (polledDetails.localIp.equals(reachableAddr)) {
|
else if (polledDetails.localAddress.equals(reachableAddr)) {
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -571,7 +578,7 @@ public class ComputerManagerService extends Service {
|
|||||||
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
||||||
if (confirmationReachTuple == null) {
|
if (confirmationReachTuple == null) {
|
||||||
// Neither of those seem to work, so we'll hold onto the address that did work
|
// Neither of those seem to work, so we'll hold onto the address that did work
|
||||||
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
|
initialReachTuple.computer.localAddress = initialReachTuple.reachableAddress;
|
||||||
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -581,8 +588,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the old MAC address
|
// Save some details about the old state of the PC that we may wish
|
||||||
|
// to restore later.
|
||||||
String savedMacAddress = details.macAddress;
|
String savedMacAddress = details.macAddress;
|
||||||
|
String savedLocalAddress = details.localAddress;
|
||||||
|
String savedRemoteAddress = details.remoteAddress;
|
||||||
|
|
||||||
// If we got here, it's reachable
|
// If we got here, it's reachable
|
||||||
details.update(initialReachTuple.computer);
|
details.update(initialReachTuple.computer);
|
||||||
@@ -593,6 +603,33 @@ public class ComputerManagerService extends Service {
|
|||||||
details.macAddress = savedMacAddress;
|
details.macAddress = savedMacAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We never want to lose IP addresses by polling server info. If we get a poll back
|
||||||
|
// where localAddress == remoteAddress but savedLocalAddress != savedRemoteAddress,
|
||||||
|
// then we've lost an address in the polling and we should restore the one that's missing.
|
||||||
|
if (details.localAddress.equals(details.remoteAddress) &&
|
||||||
|
!savedLocalAddress.equals(savedRemoteAddress)) {
|
||||||
|
if (details.localAddress.equals(savedLocalAddress)) {
|
||||||
|
// Local addresses are identical, so put the old remote address back
|
||||||
|
details.remoteAddress = savedRemoteAddress;
|
||||||
|
}
|
||||||
|
else if (details.remoteAddress.equals(savedRemoteAddress)) {
|
||||||
|
// Remote addresses are identical, so put the old local address back
|
||||||
|
details.localAddress = savedLocalAddress;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Neither IP address match. Let's restore the remote address to be safe.
|
||||||
|
details.remoteAddress = savedRemoteAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now update the reachability so the correct address is used
|
||||||
|
if (details.localAddress.equals(initialReachTuple.reachableAddress)) {
|
||||||
|
details.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
details.reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +653,7 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||||
// Add tuples for each computer
|
// Add tuples for each computer
|
||||||
addTuple(computer);
|
addTuple(computer, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
@@ -694,8 +731,6 @@ public class ComputerManagerService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
int emptyAppListResponses = 0;
|
int emptyAppListResponses = 0;
|
||||||
do {
|
do {
|
||||||
InetAddress selectedAddr;
|
|
||||||
|
|
||||||
// Can't poll if it's not online
|
// Can't poll if it's not online
|
||||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
@@ -709,19 +744,12 @@ public class ComputerManagerService extends Service {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
selectedAddr = computer.localIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectedAddr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
|
||||||
|
|
||||||
PollingTuple tuple = getPollingTuple(computer);
|
PollingTuple tuple = getPollingTuple(computer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||||
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
String appList;
|
String appList;
|
||||||
if (tuple != null) {
|
if (tuple != null) {
|
||||||
// If we're polling this machine too, grab the network lock
|
// If we're polling this machine too, grab the network lock
|
||||||
@@ -816,10 +844,10 @@ class PollingTuple {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ReachabilityTuple {
|
class ReachabilityTuple {
|
||||||
public final InetAddress reachableAddress;
|
public final String reachableAddress;
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
|
|
||||||
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
|
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.reachableAddress = reachableAddress;
|
this.reachableAddress = reachableAddress;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
convertView = inflater.inflate(layoutId, viewGroup, false);
|
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
ImageView imgView = convertView.findViewById(R.id.grid_image);
|
||||||
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
|
||||||
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
TextView txtView = convertView.findViewById(R.id.grid_text);
|
||||||
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner);
|
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
||||||
|
|
||||||
if (imgView != null) {
|
if (imgView != null) {
|
||||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import android.content.Context;
|
|||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.InetAddress;
|
|
||||||
|
|
||||||
public class NetworkAssetLoader {
|
public class NetworkAssetLoader {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@@ -21,10 +20,9 @@ public class NetworkAssetLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
|
||||||
|
|
||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
try {
|
try {
|
||||||
|
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||||
in = http.getBoxArt(tuple.app);
|
in = http.getBoxArt(tuple.app);
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
@@ -37,13 +35,4 @@ public class NetworkAssetLoader {
|
|||||||
|
|
||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
return computer.localIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return computer.remoteIp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -17,7 +14,6 @@ import android.content.ComponentName;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@@ -50,18 +46,12 @@ public class AddComputerManually extends Activity {
|
|||||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||||
getResources().getString(R.string.msg_add_pc), false);
|
getResources().getString(R.string.msg_add_pc), false);
|
||||||
|
|
||||||
try {
|
if (!managerBinder.addComputerBlocking(host, true)){
|
||||||
InetAddress addr = InetAddress.getByName(host);
|
msg = getResources().getString(R.string.addpc_fail);
|
||||||
|
}
|
||||||
if (!managerBinder.addComputerBlocking(addr)){
|
else {
|
||||||
msg = getResources().getString(R.string.addpc_fail);
|
msg = getResources().getString(R.string.addpc_success);
|
||||||
}
|
finish = true;
|
||||||
else {
|
|
||||||
msg = getResources().getString(R.string.addpc_success);
|
|
||||||
finish = true;
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
msg = getResources().getString(R.string.addpc_unknown_host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
@@ -142,7 +132,7 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
this.hostText = findViewById(R.id.hostTextView);
|
||||||
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.limelight.preferences;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
@@ -23,6 +22,7 @@ public class PreferenceConfiguration {
|
|||||||
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
||||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
||||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
||||||
|
private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
|
||||||
|
|
||||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||||
@@ -45,6 +45,7 @@ public class PreferenceConfiguration {
|
|||||||
private static final boolean DEFAULT_USB_DRIVER = true;
|
private static final boolean DEFAULT_USB_DRIVER = true;
|
||||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||||
|
private static final boolean DEFAULT_BATTERY_SAVER = false;
|
||||||
|
|
||||||
public static final int FORCE_H265_ON = -1;
|
public static final int FORCE_H265_ON = -1;
|
||||||
public static final int AUTOSELECT_H265 = 0;
|
public static final int AUTOSELECT_H265 = 0;
|
||||||
@@ -58,6 +59,7 @@ public class PreferenceConfiguration {
|
|||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||||
public boolean onscreenController;
|
public boolean onscreenController;
|
||||||
|
public boolean batterySaver;
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
public static int getDefaultBitrate(String resFpsString) {
|
||||||
if (resFpsString.equals("720p30")) {
|
if (resFpsString.equals("720p30")) {
|
||||||
@@ -128,6 +130,16 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void resetStreamingSettings(Context context) {
|
||||||
|
// We consider resolution, FPS, bitrate, and video format as "streaming settings" here
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
prefs.edit()
|
||||||
|
.remove(BITRATE_PREF_STRING)
|
||||||
|
.remove(RES_FPS_PREF_STRING)
|
||||||
|
.remove(VIDEO_FORMAT_PREF_STRING)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
public static PreferenceConfiguration readPreferences(Context context) {
|
public static PreferenceConfiguration readPreferences(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||||
@@ -188,6 +200,7 @@ public class PreferenceConfiguration {
|
|||||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
||||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
||||||
|
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
@@ -15,8 +14,6 @@ import com.limelight.PcView;
|
|||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ package com.limelight.ui;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
|
|
||||||
public interface AdapterFragmentCallbacks {
|
public interface AdapterFragmentCallbacks {
|
||||||
public int getAdapterFragmentLayoutId();
|
int getAdapterFragmentLayoutId();
|
||||||
public void receiveAbsListView(AbsListView gridView);
|
void receiveAbsListView(AbsListView gridView);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.limelight.ui;
|
package com.limelight.ui;
|
||||||
|
|
||||||
public interface GameGestures {
|
public interface GameGestures {
|
||||||
public void showKeyboard();
|
void showKeyboard();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|||||||
@@ -14,21 +14,18 @@ import com.limelight.nvstream.http.NvApp;
|
|||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
public class ServerHelper {
|
public class ServerHelper {
|
||||||
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
computer.localIp : computer.remoteIp;
|
computer.localAddress : computer.remoteAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
Intent intent = new Intent(parent, Game.class);
|
Intent intent = new Intent(parent, Game.class);
|
||||||
intent.putExtra(Game.EXTRA_HOST,
|
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
|
||||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
|
||||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
|
||||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
@@ -45,7 +42,7 @@ public class ServerHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
public static void doQuit(final Activity parent,
|
||||||
final InetAddress address,
|
final String address,
|
||||||
final NvApp app,
|
final NvApp app,
|
||||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
final Runnable onComplete) {
|
final Runnable onComplete) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 14 KiB |
@@ -6,7 +6,7 @@
|
|||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
tools:context=".AddComputerManually" >
|
tools:context=".preferences.AddComputerManually" >
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/manuallyAddPcText"
|
android:id="@+id/manuallyAddPcText"
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/activity_help"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
|
||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
|
||||||
tools:context="com.limelight.HelpActivity">
|
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_alignParentStart="true" />
|
|
||||||
</RelativeLayout>
|
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
android:id="@+id/stream_settings"
|
android:id="@+id/stream_settings"
|
||||||
tools:context=".StreamSettings">
|
tools:context=".preferences.StreamSettings">
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_pc_scut_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_pc_scut_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 715 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 471 B |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 816 B |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#565C64</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_pc_scut_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -47,6 +47,10 @@
|
|||||||
<string name="error_404">GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU.
|
<string name="error_404">GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU.
|
||||||
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
|
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
|
||||||
</string>
|
</string>
|
||||||
|
<string name="title_decoding_error">Video Decoder Crashed</string>
|
||||||
|
<string name="message_decoding_error">Moonlight has crashed due to a problem with this device\'s video decoder. Try adjusting the streaming settings if the crashes continue.</string>
|
||||||
|
<string name="title_decoding_reset">Video Settings Reset</string>
|
||||||
|
<string name="message_decoding_reset">Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default.</string>
|
||||||
|
|
||||||
<!-- Start application messages -->
|
<!-- Start application messages -->
|
||||||
<string name="conn_establishing_title">Establishing Connection</string>
|
<string name="conn_establishing_title">Establishing Connection</string>
|
||||||
@@ -104,6 +108,8 @@
|
|||||||
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
|
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
|
||||||
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
|
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
|
||||||
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
|
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
|
||||||
|
<string name="title_checkbox_battery_saver">Battery saver</string>
|
||||||
|
<string name="summary_checkbox_battery_saver">Uses less battery, but may increase stuttering</string>
|
||||||
|
|
||||||
<string name="category_audio_settings">Audio Settings</string>
|
<string name="category_audio_settings">Audio Settings</string>
|
||||||
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
|
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
android:title="@string/title_checkbox_stretch_video"
|
android:title="@string/title_checkbox_stretch_video"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:key="checkbox_disable_warnings"
|
android:key="checkbox_battery_saver"
|
||||||
android:title="@string/title_checkbox_disable_warnings"
|
android:title="@string/title_checkbox_battery_saver"
|
||||||
android:summary="@string/summary_checkbox_disable_warnings"
|
android:summary="@string/summary_checkbox_battery_saver"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory android:title="@string/category_audio_settings">
|
<PreferenceCategory android:title="@string/category_audio_settings">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:2.3.3'
|
classpath 'com.android.tools.build:gradle:3.0.0-beta5'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#Thu Mar 02 18:32:01 PST 2017
|
#Sun Sep 03 12:51:12 PDT 2017
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
|
||||||
<component name="FacetManager">
|
<component name="FacetManager">
|
||||||
<facet type="java-gradle" name="Java-Gradle">
|
<facet type="java-gradle" name="Java-Gradle">
|
||||||
<configuration>
|
<configuration>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 73 KiB |
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.0"
|
||||||
|
width="546.083726pt"
|
||||||
|
height="546.155759pt"
|
||||||
|
viewBox="0 0 546.083726 546.155759"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
id="svg4059"
|
||||||
|
sodipodi:docname="lime_layer.svg"
|
||||||
|
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
|
||||||
|
<defs
|
||||||
|
id="defs4063" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="951"
|
||||||
|
id="namedview4061"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.32408337"
|
||||||
|
inkscape:cx="364.05583"
|
||||||
|
inkscape:cy="364.10384"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4059" />
|
||||||
|
<metadata
|
||||||
|
id="metadata4039">
|
||||||
|
Created by potrace 1.12, written by Peter Selinger 2001-2015
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
transform="translate(-202.416274,747.655759) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000"
|
||||||
|
stroke="none"
|
||||||
|
id="g4057"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
<path
|
||||||
|
d="M4550 7473 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z"
|
||||||
|
id="path4041"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="M4867 7473 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z"
|
||||||
|
id="path4043"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="M2673 6518 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z"
|
||||||
|
id="path4045"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="M5898 5733 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z"
|
||||||
|
id="path4047"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="M2026 4631 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z"
|
||||||
|
id="path4049"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="M5030 4633 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z"
|
||||||
|
id="path4051"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="M3767 3602 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z"
|
||||||
|
id="path4053"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="M4862 3243 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z"
|
||||||
|
id="path4055"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |