Compare commits

...
68 Commits
Author SHA1 Message Date
Cameron Gutman b9f1142af7 Version 5.2 2017-09-09 18:53:36 -07:00
Cameron Gutman 38a6a2b74a A few fixes for decoder crash notifications 2017-09-09 18:44:06 -07:00
Cameron Gutman fd2421618a Update common-c with crash fix 2017-09-09 17:40:53 -07:00
Cameron Gutman 79a9ea7179 Add decoder crash notification and settings reset on continued crashing 2017-09-09 17:40:07 -07:00
Cameron Gutman 34a11c9262 Correct reachability when restoring a lost address 2017-09-09 16:02:39 -07:00
Cameron Gutman 84a9845c1d Fix polling overwriting manually entered IP addresses 2017-09-09 15:40:07 -07:00
Cameron Gutman 5b05220008 Prevent mDNS from overwriting external IP addresses 2017-09-09 15:21:31 -07:00
Cameron Gutman b2bd7257e1 Fix Lint warnings 2017-09-09 14:12:54 -07:00
Cameron Gutman 46a998c113 Convert address fields to strings to better manage DNS names 2017-09-09 13:39:54 -07:00
Cameron Gutman 60cd951774 Rename localIp/remoteIp fields to localAddress/remoteAddress to prepare for DNS names 2017-09-09 12:47:23 -07:00
Cameron Gutman d4f8d8f689 Switch database storage to use strings for addresses 2017-09-09 12:43:20 -07:00
Cameron Gutman 608a0ebb5b Update build files for AS3b5 2017-09-09 11:50:42 -07:00
Cameron Gutman f01a15d182 Removed duplicated current address logic 2017-09-09 11:49:15 -07:00
Cameron Gutman 0268b4f958 Update gradle for AS 3.0b4 2017-09-03 12:52:18 -07:00
Cameron Gutman d71cf0eb98 Add app category for Oreo 2017-09-02 13:48:45 -07:00
Cameron Gutman 10ab40f823 Add/update remaining assets 2017-09-02 13:48:11 -07:00
Cameron Gutman 427edfa021 Update common submodule 2017-09-01 19:11:49 -07:00
Cameron Gutman 6f18831d5c Update BouncyCastle libs 2017-09-01 18:39:49 -07:00
Cameron Gutman a3db09f422 Disable input compatibility mode on ChromeOS 2017-09-01 18:07:18 -07:00
Cameron Gutman d185a05b1d Sort and sync vendor IDs with xpad 2017-08-25 21:04:36 -07:00
Cameron Gutman 78e575504a Update straggling app icon 2017-08-23 23:07:03 -07:00
Cameron Gutman 0a0be19b69 Fix brown-paper-bag bug in audio init error checking 2017-08-22 00:17:03 -07:00
Cameron Gutman 0792157e9d Fix some markdown errors and tweak supported GPUs 2017-08-13 23:53:18 -07:00
madmario1000 cdd0ecf0b7 Update README.md (#400)
Clarify the required specs a bit
2017-08-13 23:49:49 -07:00
Cameron Gutman 1ac721a35b Bump to version 5.1.2 2017-08-13 18:51:22 -07:00
Cameron Gutman e49b1c92a2 Update for AS 3.0 Beta 2 2017-08-13 18:37:31 -07:00
Cameron Gutman db4295bf83 Add adaptive icon for PC shortcut 2017-08-13 18:31:09 -07:00
Cameron Gutman 824c37f9d5 Adaptive launcher icon 2017-08-13 18:06:53 -07:00
Cameron Gutman acf4426952 Update for Gradle 4 2017-06-24 12:56:52 -07:00
Cameron Gutman e8c50342ab Version 5.1.1 2017-06-17 16:06:57 -07:00
Cameron Gutman 598995de3b Fix audio renderer using non-existant classes on Lollipop 2017-06-16 20:27:03 -07:00
Cameron Gutman 01cf0cc649 Fix Lint error 2017-06-16 20:06:45 -07:00
Cameron Gutman fa560f462f Add battery saver mode 2017-06-16 20:01:41 -07:00
Cameron Gutman f6e40118a9 Bring back the warning displayed if video decoder initialization fails 2017-06-16 19:50:50 -07:00
Cameron Gutman fe7148dbd4 Only throw decoder exceptions if we're still receiving them after 3 seconds 2017-06-16 19:39:15 -07:00
Cameron Gutman 60de065836 Cleanup video decoder teardown paths 2017-06-16 19:11:39 -07:00
Cameron Gutman 6f82f82abb Use low latency audio pathway on Lollipop and later 2017-06-16 19:08:15 -07:00
Cameron Gutman 42f18cb4ac Version 5.1 2017-06-11 17:14:15 -07:00
Cameron Gutman 1bbd0054c2 Update common again to fix another long haul testing bug 2017-06-11 14:56:41 -07:00
Cameron Gutman acdde37a3a Update common library again 2017-06-11 13:54:05 -07:00
Cameron Gutman ad40e12167 Update common to fix incorrect assert firing 2017-06-11 13:09:29 -07:00
Cameron Gutman 1b3322b5ee Suppress crashes if the surface has become invalid 2017-06-10 17:25:23 -07:00
Cameron Gutman 6340ec6c6d Consolidate handling of decoder exceptions 2017-06-10 16:57:37 -07:00
Cameron Gutman babd92c8c0 Add additional information to total frame latency and RendererException 2017-06-10 16:45:07 -07:00
Cameron Gutman 7f1fe5f520 Fix NPE if the device doesn't support H.264 hardware decoding 2017-06-10 11:48:25 -07:00
Cameron Gutman 01458770d2 Fix NPE enumerating input devices 2017-06-10 11:45:12 -07:00
Cameron Gutman 8d05f044f5 Allow software decoding on the emulator for testing 2017-06-08 22:21:51 -07:00
Cameron Gutman f5680b59a5 Use debug moonlight-common with asserts enabled on debug builds and release moonlight-common with asserts disabled on release builds 2017-06-08 19:57:55 -07:00
Cameron Gutman 0ecf86c7ed At long last, Android has native mouse capture. Don't show the root version to users running O 2017-06-08 18:26:12 -07:00
Cameron Gutman 6789e8d497 Immediately call stopConnection() rather than waiting for activity stop on connection failure 2017-06-08 18:24:22 -07:00
Cameron Gutman 7d0160d556 Update gradle and SDK to O 2017-06-08 18:17:59 -07:00
Cameron Gutman f6a0990432 Final fixes for Android O pointer capture 2017-06-08 18:17:34 -07:00
Cameron Gutman 5d6094df97 Version 5.0.2 2017-06-08 17:57:57 -07:00
Cameron Gutman d98d4aeda2 Fix FEC fencepost error in moonlight-common 2017-06-08 17:57:43 -07:00
Cameron Gutman 852dcf5a2d Merge branch 'o-bringup' 2017-06-08 17:30:10 -07:00
Cameron Gutman 82e5aa122d Update common with FEC and latency fixes 2017-06-07 23:17:06 -07:00
Cameron Gutman fe237d1da3 Fix some exceptions that escaped on decoder shutdown and surface loss 2017-06-07 20:01:09 -07:00
Cameron Gutman e199fcd2d9 Try allowing decoder exceptions after initial start since we shouldn't throw on stop anymore 2017-06-06 22:50:08 -07:00
Cameron Gutman d7c6f63592 Force Qualcomm and Samsung HEVC decoders disabled to avoid crashes and poor performance 2017-06-06 22:49:09 -07:00
Cameron Gutman 4b9c6b149a Remove the decoder stop hack and try to workaround the issue differently 2017-06-06 22:48:28 -07:00
Cameron Gutman d1e41e41a1 Stop the connection in onStop() to try to avoid deadlocks due to surface loss. Also avoid calling stopConnection() from connection listener callbacks due to deadlock risk. 2017-06-05 20:33:23 -07:00
Cameron Gutman 96dfe25a14 Support packet size adjustments on LANs 2017-06-03 11:51:35 -07:00
Cameron Gutman f76d78607a Improve HEVC decoder compatibility by submitting VPS+SPS+PPS in one CSD blob rather than individually 2017-06-03 11:46:29 -07:00
Cameron Gutman 90a1e68c68 Move input capture check to not mask touch events 2017-06-02 18:17:18 -07:00
Cameron Gutman a413185085 Fix Pixel C keyboard d-pad regression due to aliasing with SOURCE_GAMEPAD 2017-05-31 21:51:01 -07:00
Cameron Gutman aa1b283570 Initial working pointer capture using onClick 2017-05-31 21:46:53 -07:00
Cameron Gutman f07c886711 Add isCapturing() method to mouse capture providers 2017-05-31 21:26:26 -07:00
Cameron Gutman e66b1ebec9 Initial pointer capture work for O 2017-05-31 20:50:47 -07:00
80 changed files with 936 additions and 423 deletions
+10 -10
View File
@@ -1,4 +1,4 @@
#Moonlight
# Moonlight
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
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.
##Features
## Features
* Streams any of your games from your PC to your Android device
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network
##Installation
## Installation
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
* 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
* High-end wireless router (802.11n dual-band recommended)
##Usage
## Usage
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* 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
* Play games!
##Contribute
## Contribute
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
3. Send Pull Requests
##Building
## Building
* Install Android Studio and the Android NDK
* 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.
* Build the APK using Android Studio
##Authors
## Authors
* [Cameron Gutman](https://github.com/cgutman)
* [Diego Waxemberg](https://github.com/dwaxemberg)
+20 -9
View File
@@ -4,23 +4,31 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion '25.0.3'
compileSdkVersion 26
buildToolsVersion '26.0.1'
defaultConfig {
minSdkVersion 16
targetSdkVersion 25
targetSdkVersion 26
versionName "5.0.1"
versionCode = 123
versionName "5.2"
versionCode = 131
}
flavorDimensions "root"
productFlavors {
root {
// Android O has native mouse capture, so don't show the rooted
// version to devices running O on the Play Store.
maxSdkVersion 25
applicationId "com.limelight.root"
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
}
dimension "root"
}
nonRoot {
@@ -28,6 +36,8 @@ android {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
}
dimension "root"
}
}
@@ -50,8 +60,9 @@ android {
}
dependencies {
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
compile 'org.bouncycastle:bcpkix-jdk15on:1.52'
compile project(':moonlight-common')
compile files('libs/jcodec-0.1.9-patched.jar')
implementation 'org.bouncycastle:bcprov-jdk15on:1.57'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.57'
implementation files('libs/jcodec-0.1.9-patched.jar')
implementation project(':moonlight-common')
}
+8 -1
View File
@@ -24,12 +24,19 @@
android:name="android.software.leanback"
android:required="false" />
<!-- Disable legacy input emulation on ChromeOS -->
<uses-feature
android:name="android.hardware.type.pc"
android:required="false"/>
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:isGame="true"
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">
<!-- Samsung multi-window support -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+1 -3
View File
@@ -2,7 +2,6 @@ package com.limelight;
import java.io.StringReader;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.computers.ComputerManagerListener;
@@ -27,7 +26,6 @@ import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
@@ -250,7 +248,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
String computerName = getIntent().getStringExtra(NAME_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
TextView label = (TextView) findViewById(R.id.appListText);
TextView label = findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
+96 -36
View File
@@ -10,6 +10,7 @@ import com.limelight.binding.input.TouchContext;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.binding.input.evdev.EvdevListener;
import com.limelight.binding.input.virtual_controller.VirtualController;
import com.limelight.binding.video.CrashListener;
import com.limelight.binding.video.MediaCodecDecoderRenderer;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.nvstream.NvConnection;
@@ -34,6 +35,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.graphics.Point;
import android.hardware.input.InputManager;
import android.media.AudioManager;
@@ -81,6 +83,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private VirtualController virtualController;
private PreferenceConfiguration prefConfig;
private SharedPreferences tombstonePrefs;
private NvConnection conn;
private SpinnerDialog spinner;
@@ -165,12 +168,28 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Read the stream preferences
prefConfig = PreferenceConfiguration.readPreferences(this);
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
// Listen for events on the game surface
streamView = (StreamView) findViewById(R.id.surfaceView);
streamView = findViewById(R.id.surfaceView);
streamView.setOnGenericMotionListener(this);
streamView.setOnTouchListener(this);
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// The view must be focusable for pointer capture to work.
streamView.setFocusable(true);
streamView.setDefaultFocusHighlightEnabled(false);
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
@Override
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
return handleMotionEvent(motionEvent);
}
});
}
// Warn the user if they're on a metered connection
checkDataConnection();
@@ -200,7 +219,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the MediaCodec helper before creating the decoder
MediaCodecHelper.initializeWithContext(this);
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat);
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat,
prefConfig.bitrate,
prefConfig.batterySaver,
new CrashListener() {
@Override
public void notifyCrash(Exception e) {
// The MediaCodec instance is going down due to a crash
// let's tell the user something when they open the app again
// We must use commit because the app will crash when we return from this function
tombstonePrefs.edit().putInt("CrashCount", tombstonePrefs.getInt("CrashCount", 0) + 1).commit();
}
},
tombstonePrefs.getInt("CrashCount", 0));
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
@@ -257,12 +289,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
getWindow().setSustainedPerformanceMode(true);
}
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
if (prefConfig.onscreenController) {
// create virtual onscreen controller
virtualController = new VirtualController(conn,
(FrameLayout)findViewById(R.id.surfaceView).getParent(),
(FrameLayout)streamView.getParent(),
this);
virtualController.refreshLayout();
}
@@ -277,6 +307,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.getHolder().addCallback(this);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Capture is lost when focus is lost, so it must be requested again
// when focus is regained.
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
// Recapture the pointer if focus was regained
streamView.requestPointerCapture();
}
}
}
private void prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
@@ -420,6 +464,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
unbindService(usbDriverServiceConnection);
}
// Destroy the capture provider
inputCaptureProvider.destroy();
}
@Override
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (conn != null) {
int videoFormat = decoderRenderer.getActiveVideoFormat();
@@ -452,15 +507,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
// Clear the tombstone count
if (tombstonePrefs.getInt("CrashCount", 0) != 0) {
tombstonePrefs.edit().putInt("CrashCount", 0).apply();
}
}
}
@Override
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
finish();
}
@@ -567,9 +619,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
boolean handled = false;
boolean detectedGamepad = event.getDevice() == null ? false :
((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
if (detectedGamepad || (event.getDevice() == null ||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
)) {
@@ -609,9 +660,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
boolean handled = false;
boolean detectedGamepad = event.getDevice() == null ? false :
((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
if (detectedGamepad || (event.getDevice() == null ||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
)) {
@@ -671,7 +721,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
}
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
{
// This case is for mice
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
@@ -680,6 +731,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
int changedButtons = event.getButtonState() ^ lastButtonState;
// Ignore mouse input if we're not capturing from our input source
if (!inputCaptureProvider.isCapturingActive()) {
return false;
}
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
@@ -893,12 +949,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
connecting = connected = false;
conn.stop();
}
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
// Destroy the capture provider
inputCaptureProvider.destroy();
}
@Override
@@ -908,11 +958,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
spinner = null;
}
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe(stage+" failed: "+errorCode);
stopConnection();
// 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),
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
}
@@ -920,11 +982,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void connectionTerminated(long errorCode) {
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe("Connection terminated: "+errorCode);
stopConnection();
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
@@ -991,15 +1056,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (connected) {
// HACK: Android is supposed to let you return from this function
// before throwing a fit if you access the surface again. Unfortunately,
// MediaCodec often tries to access the destroyed surface and triggers
// an IllegalStateException. To workaround this, we will invoke
// the DecoderRenderer's stop function ourselves, so it will hopefully
// happen early enough to not trigger the bug
decoderRenderer.stop();
// Let the decoder know immediately that the surface is gone
decoderRenderer.prepareForStop();
if (connected) {
stopConnection();
}
}
@@ -51,14 +51,8 @@ public class HelpActivity extends Activity {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase())) {
// Allow navigation to Moonlight docs
return false;
}
else {
return true;
}
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()));
}
});
+33 -32
View File
@@ -2,9 +2,7 @@ package com.limelight;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
@@ -33,6 +31,7 @@ import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
@@ -110,9 +109,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Setup the list view
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
ImageButton helpButton = (ImageButton) findViewById(R.id.helpButton);
ImageButton settingsButton = findViewById(R.id.settingsButton);
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
ImageButton helpButton = findViewById(R.id.helpButton);
settingsButton.setOnClickListener(new OnClickListener() {
@Override
@@ -138,7 +137,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
.replace(R.id.pcFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE);
}
@@ -165,6 +164,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
PreferenceConfiguration.readPreferences(this).smallIconMode);
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() {
@@ -306,19 +331,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Stop updates and wait while pairing
stopComputerUpdates(true);
InetAddress addr;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this));
@@ -443,19 +456,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
NvHTTP httpConn;
String message;
try {
InetAddress addr;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this));
@@ -1,8 +1,10 @@
package com.limelight.binding.audio;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Build;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer;
@@ -12,10 +14,58 @@ public class AndroidAudioRenderer implements AudioRenderer {
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
public int setup(int audioConfiguration) {
int channelConfig;
int bufferSize;
int bytesPerFrame;
switch (audioConfiguration)
@@ -38,44 +88,82 @@ public class AndroidAudioRenderer implements AudioRenderer {
// do this on many devices and it lowers audio latency.
// We'll try the small buffer size first and if it fails,
// use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = bytesPerFrame * 2;
track = new AudioTrack(AudioManager.STREAM_MUSIC,
48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
try {
if (track != null) {
track.release();
}
} catch (Exception ignored) {}
for (int i = 0; i < 4; i++) {
boolean lowLatency;
int bufferSize;
// Now try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
// We will try:
// 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,
AudioFormat.ENCODING_PCM_16BIT),
bytesPerFrame * 2);
bytesPerFrame * 2);
// Round to next frame
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
// Round to next frame
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
break;
default:
// Unreachable
throw new IllegalStateException();
}
track = new AudioTrack(AudioManager.STREAM_MUSIC,
48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
// Skip low latency options if hardware sample rate isn't 48000Hz
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
continue;
}
try {
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;
}
@@ -63,6 +63,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
int[] ids = InputDevice.getDeviceIds();
for (int id : ids) {
InputDevice dev = InputDevice.getDevice(id);
if (dev == null) {
// This device was removed during enumeration
continue;
}
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure
@@ -2,8 +2,6 @@ package com.limelight.binding.input;
import android.view.KeyEvent;
import com.limelight.nvstream.NvConnection;
/**
* Class to translate a Android key code into the codes GFE is expecting
* @author Diego Waxemberg
@@ -0,0 +1,53 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
@TargetApi(Build.VERSION_CODES.O)
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
private View targetView;
public AndroidNativePointerCaptureProvider(View targetView) {
this.targetView = targetView;
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
@Override
public void enableCapture() {
super.enableCapture();
targetView.requestPointerCapture();
}
@Override
public void disableCapture() {
super.disableCapture();
targetView.releasePointerCapture();
}
@Override
public boolean isCapturingActive() {
return targetView.hasPointerCapture();
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getX();
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getY();
}
}
@@ -10,11 +10,11 @@ import android.view.View;
import android.view.ViewGroup;
@TargetApi(Build.VERSION_CODES.N)
public class AndroidCaptureProvider extends InputCaptureProvider {
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
private ViewGroup rootViewGroup;
private Context context;
public AndroidCaptureProvider(Activity activity) {
public AndroidPointerIconCaptureProvider(Activity activity) {
this.context = activity;
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
}
@@ -33,11 +33,13 @@ public class AndroidCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {
super.enableCapture();
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
}
@Override
public void disableCapture() {
super.disableCapture();
setPointerIconOnAllViews(null);
}
@@ -3,15 +3,17 @@ package com.limelight.binding.input.capture;
import android.app.Activity;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
import com.limelight.binding.input.evdev.EvdevListener;
public class InputCaptureManager {
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
// Shield capture is preferred because it can capture when the cursor is over
// the system UI. Android N native capture can only capture over views owned
// by the application.
if (ShieldCaptureProvider.isCaptureProviderSupported()) {
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android O+ native mouse capture");
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
}
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity);
}
@@ -19,9 +21,11 @@ public class InputCaptureManager {
LimeLog.info("Using Evdev mouse capture");
return new EvdevCaptureProvider(activity, rootListener);
}
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android N+ native mouse capture");
return new AndroidCaptureProvider(activity);
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
// Android N's native capture can't capture over system UI elements
// so we want to only use it if there's no other option.
LimeLog.info("Using Android N+ pointer hiding");
return new AndroidPointerIconCaptureProvider(activity);
}
else {
LimeLog.info("Mouse capture not available");
@@ -3,10 +3,25 @@ package com.limelight.binding.input.capture;
import android.view.MotionEvent;
public abstract class InputCaptureProvider {
public abstract void enableCapture();
public abstract void disableCapture();
protected boolean isCapturing;
public void enableCapture() {
isCapturing = true;
}
public void disableCapture() {
isCapturing = false;
}
public void destroy() {}
public boolean isCapturingEnabled() {
return isCapturing;
}
public boolean isCapturingActive() {
return isCapturing;
}
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return false;
}
@@ -1,10 +1,4 @@
package com.limelight.binding.input.capture;
public class NullCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {}
@Override
public void disableCapture() {}
}
public class NullCaptureProvider extends InputCaptureProvider {}
@@ -63,11 +63,13 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {
super.enableCapture();
setCursorVisibility(false);
}
@Override
public void disableCapture() {
super.disableCapture();
setCursorVisibility(true);
}
@@ -17,18 +17,22 @@ public class Xbox360Controller extends AbstractXboxController {
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // Unknown
0x0f0d, // Hori
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1bad, // Harmonix
0x0f0d, // Hori
0x1689, // Razer Onza
0x24c6, // PowerA
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1bad, // Harmonix
0x24c6, // PowerA
};
public static boolean canClaimDevice(UsbDevice device) {
@@ -19,6 +19,7 @@ public class XboxOneController extends AbstractXboxController {
0x0738, // Mad Catz
0x0e6f, // Unknown
0x0f0d, // Hori
0x1532, // Razer Wildcat
0x24c6, // PowerA
};
@@ -221,6 +221,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {
super.enableCapture();
if (!started) {
// Start the handler thread if it's our first time
// capturing
@@ -247,6 +248,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
@Override
public void disableCapture() {
super.disableCapture();
// This may be called on the main thread
runInNetworkSafeContextSynchronously(new Runnable() {
@Override
@@ -1,12 +1,12 @@
package com.limelight.binding.input.evdev;
public interface EvdevListener {
public static final int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3;
int BUTTON_LEFT = 1;
int BUTTON_MIDDLE = 2;
int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode);
void mouseMove(int deltaX, int deltaY);
void mouseButtonEvent(int buttonId, boolean down);
void mouseScroll(byte amount);
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.preferences.PreferenceConfiguration;
import android.content.SharedPreferences;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
@@ -23,6 +24,7 @@ import android.view.SurfaceHolder;
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private static final boolean USE_FRAME_RENDER_TIME = false;
private static final boolean FRAME_RENDER_TIME_ONLY = USE_FRAME_RENDER_TIME && false;
// Used on versions < 5.0
private ByteBuffer[] legacyInputBuffers;
@@ -30,6 +32,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private MediaCodecInfo avcDecoder;
private MediaCodecInfo hevcDecoder;
// Used for HEVC only
private byte[] vpsBuffer;
private byte[] spsBuffer;
private MediaCodec videoDecoder;
private Thread rendererThread;
private Thread[] spinnerThreads;
@@ -40,16 +46,28 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private boolean refFrameInvalidationActive;
private int initialWidth, initialHeight;
private int videoFormat;
private Object renderTarget;
private boolean stopping;
private SurfaceHolder renderTarget;
private volatile boolean stopping;
private CrashListener crashListener;
private boolean reportedCrash;
private int consecutiveCrashCount;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
private RendererException initialException;
private long initialExceptionTimestamp;
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
private long lastTimestampUs;
private long decoderTimeMs;
private long totalTimeMs;
private int totalFrames;
private int frameLossEvents;
private int framesLost;
private int lastFrameNumber;
private int refreshRate;
private int bitrate;
private int numSpsIn;
private int numPpsIn;
@@ -91,14 +109,25 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return decoderInfo;
}
public void setRenderTarget(Object renderTarget) {
public void setRenderTarget(SurfaceHolder renderTarget) {
this.renderTarget = renderTarget;
}
public MediaCodecDecoderRenderer(int videoFormat) {
public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver,
CrashListener crashListener, int consecutiveCrashCount) {
//dumpDecoders();
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
this.bitrate = bitrate;
this.crashListener = crashListener;
this.consecutiveCrashCount = consecutiveCrashCount;
// Disable spinner threads in battery saver mode
if (batterySaver) {
spinnerThreads = new Thread[0];
}
else {
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
}
avcDecoder = findAvcDecoder();
if (avcDecoder != null) {
@@ -156,6 +185,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
this.initialWidth = width;
this.initialHeight = height;
this.videoFormat = format;
this.refreshRate = redrawRate;
String mimeType;
String selectedDecoderName;
@@ -233,7 +263,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
try {
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -266,12 +296,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return 0;
}
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags, boolean throwOnTransient) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
if (codecExc.isTransient()) {
if (codecExc.isTransient() && !throwOnTransient) {
// We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo());
return;
@@ -281,14 +311,35 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
}
// Only throw if this happens at the beginning of a stream
// but not if we're stopping
if (totalFrames > 0 && totalFrames < 20 && !stopping) {
if (buf != null || codecFlags != 0) {
throw new RendererException(this, e, buf, codecFlags);
// Only throw if we're not stopping
if (!stopping) {
//
// 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 {
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();
}
}
}
@@ -299,7 +350,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@Override
public void run() {
BufferInfo info = new BufferInfo();
while (!isInterrupted()) {
while (!stopping) {
try {
// Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
@@ -339,7 +390,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
handleDecoderException(e, null, 0, false);
}
}
}
@@ -357,7 +408,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
public void run() {
// This thread exists to keep the CPU at a higher DVFS state on devices
// where the governor scales clock speed sporadically, causing dropped frames.
while (!isInterrupted()) {
while (!stopping) {
try {
Thread.sleep(0, 1);
} catch (InterruptedException e) {
@@ -379,11 +430,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
startTime = MediaCodecHelper.getMonotonicMillis();
try {
while (rendererThread.isAlive() && index < 0 && !stopping) {
while (index < 0 && !stopping) {
index = videoDecoder.dequeueInputBuffer(10000);
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
handleDecoderException(e, null, 0, true);
return MediaCodec.INFO_TRY_AGAIN_LATER;
}
@@ -406,16 +457,41 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
startSpinnerThreads();
}
@Override
public void stop() {
// !!! May be called even if setup()/start() fails !!!
public void prepareForStop() {
// Let the decoding code know to ignore codec exceptions now
stopping = true;
// Halt the rendering thread
rendererThread.interrupt();
if (rendererThread != null) {
rendererThread.interrupt();
}
}
@Override
public void stop() {
// May be called already, but we'll call it now to be safe
prepareForStop();
try {
// Invalidate pending decode buffers
videoDecoder.flush();
} catch (Exception e) {
e.printStackTrace();
}
// Wait for the renderer thread to shut down
try {
rendererThread.join();
} catch (InterruptedException ignored) { }
try {
// Stop the video decoder
videoDecoder.stop();
} catch (Exception e) {
e.printStackTrace();
}
// Halt the spinner threads
for (Thread t : spinnerThreads) {
t.interrupt();
@@ -433,31 +509,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
// Try 25 times to submit the input buffer before throwing a real exception
int i;
Exception lastException = null;
for (i = 0; i < 25; i++) {
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
offset, length,
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(e, null, codecFlags);
lastException = e;
}
}
if (i == 25 && totalFrames > 0 && totalFrames < 20 && !stopping) {
throw new RendererException(this, lastException, null, codecFlags);
}
else if (i != 25) {
// Queued input buffer
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
offset, length,
timestampUs, codecFlags);
return true;
}
else {
// Failed to queue
} catch (Exception e) {
handleDecoderException(e, null, codecFlags, true);
return false;
}
}
@@ -468,7 +526,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
ByteBuffer buf;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
buf = videoDecoder.getInputBuffer(inputBufferIndex);
try {
buf = videoDecoder.getInputBuffer(inputBufferIndex);
} catch (Exception e) {
handleDecoderException(e, null, 0, true);
return null;
}
}
else {
buf = legacyInputBuffers[inputBufferIndex];
@@ -499,25 +562,36 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@SuppressWarnings("deprecation")
@Override
public int submitDecodeUnit(byte[] frameData, int frameLength) {
public int submitDecodeUnit(byte[] frameData, int frameLength, int frameNumber, long receiveTimeMs) {
totalFrames++;
// We can receive the same "frame" multiple times if it's an IDR frame.
// In that case, each frame start NALU is submitted independently.
if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
framesLost += frameNumber - lastFrameNumber - 1;
frameLossEvents++;
}
lastFrameNumber = frameNumber;
int inputBufferIndex;
ByteBuffer buf;
long timestampUs = System.nanoTime() / 1000;
if (!FRAME_RENDER_TIME_ONLY) {
// Count time from first packet received to decode start
totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
}
if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing
timestampUs = lastTimestampUs + 1;
}
lastTimestampUs = timestampUs;
int inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_OK;
}
ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex);
int codecFlags = 0;
boolean needsSpsReplay = false;
@@ -619,6 +693,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// Patch the SPS constraint flags
doProfileSpecificSpsPatching(sps);
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// Write the annex B header
buf.put(frameData, 0, 5);
@@ -641,6 +727,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
numPpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
if (needsBaselineSpsHack) {
LimeLog.info("Saw PPS; disabling SPS hack");
@@ -652,22 +749,66 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
else if (frameData[4] == 0x40) {
numVpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
// Batch this to submit together with SPS and PPS per AOSP docs
vpsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, vpsBuffer, 0, frameLength);
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x42) {
numSpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
// Batch this to submit together with VPS and PPS per AOSP docs
spsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, spsBuffer, 0, frameLength);
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x44) {
numPpsIn++;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// When we get the PPS, submit the VPS and SPS together with
// the PPS, as required by AOSP docs on use of HEVC and MediaCodec.
if (vpsBuffer != null) {
buf.put(vpsBuffer);
}
if (spsBuffer != null) {
buf.put(spsBuffer);
}
// This is the HEVC CSD blob
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
}
else {
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
}
// Copy data from our buffer list into the input buffer
buf.put(frameData, 0, frameLength);
if (!queueInputBuffer(inputBufferIndex,
0, frameLength,
0, buf.position(),
timestampUs, codecFlags)) {
return MoonBridge.DR_NEED_IDR;
}
@@ -690,6 +831,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
if (inputBuffer == null) {
return false;
}
// Write the Annex B header
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
@@ -778,9 +922,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "Format: "+renderer.videoFormat+"\n";
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.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 += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.bitrate+" Mbps \n";
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n";
str += "Frame losses: "+renderer.framesLost+" in "+frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
@@ -63,15 +63,24 @@ public class MediaCodecHelper {
static {
blacklistedDecoderPrefixes = new LinkedList<>();
// Software decoders that don't support H264 high profile
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
// Blacklist software decoders that don't support H264 high profile,
// but exclude the official AOSP emulator from this restriction.
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
// decoder. While not strictly necessary, I'm going to fully blacklist this
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
// Force these decoders disabled because:
// 1) They are software decoders, so the performance is terrible
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
}
static {
@@ -98,6 +107,11 @@ public class MediaCodecHelper {
static {
whitelistedHevcDecoders = new LinkedList<>();
// Allow software HEVC decoding in the official AOSP emulator
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
whitelistedHevcDecoders.add("omx.google");
}
// Exynos seems to be the only HEVC decoder that works reliably
whitelistedHevcDecoders.add("omx.exynos");
@@ -25,6 +25,8 @@ public class ComputerDatabaseManager {
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac";
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) {
@@ -60,50 +62,74 @@ public class ComputerDatabaseManager {
ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
values.put(MAC_COLUMN_NAME, details.macAddress);
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() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
ComputerDetails details = getComputerFromCursor(c);
// 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) {
continue;
}
@@ -119,46 +145,17 @@ public class ComputerDatabaseManager {
public ComputerDetails getComputerByName(String name) {
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()) {
// No matching computer
c.close();
return null;
}
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);
ComputerDetails details = getComputerFromCursor(c);
c.close();
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// 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) {
deleteComputer(details.name);
return null;
@@ -3,5 +3,5 @@ package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails;
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.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
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.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.ServerHelper;
import android.app.Service;
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;
}
@@ -210,8 +210,8 @@ public class ComputerManagerService extends Service {
}
}
public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr);
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
}
public void removeComputer(String name) {
@@ -289,7 +289,7 @@ public class ComputerManagerService extends Service {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress());
addComputerBlocking(computer.getAddress().getHostAddress(), false);
}
@Override
@@ -305,15 +305,22 @@ public class ComputerManagerService extends Service {
};
}
private void addTuple(ComputerDetails details) {
private void addTuple(ComputerDetails details, boolean manuallyAdded) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer
if (tuple.computer.uuid.equals(details.uuid)) {
// Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information
tuple.computer.localIp = details.localIp;
tuple.computer.remoteIp = details.remoteIp;
if (manuallyAdded) {
// Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information
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
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
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
fakeDetails.localAddress = addr;
fakeDetails.remoteAddress = addr;
// Block while we try to fill the details
try {
@@ -356,7 +363,7 @@ public class ComputerManagerService extends Service {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine
addTuple(fakeDetails);
addTuple(fakeDetails, manuallyAdded);
return true;
}
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
if (!fastPollIp(ipAddr)) {
if (!fastPollIp(address)) {
return null;
}
try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
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
// GFE server
private boolean fastPollIp(InetAddress addr) {
private boolean fastPollIp(String address) {
Socket s = new Socket();
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();
return true;
} 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() {
@Override
public void run() {
boolean pollRes = fastPollIp(addr);
boolean pollRes = fastPollIp(address);
synchronized (info) {
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();
}
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[] localInfo = new boolean[2];
startFastPollThread(local, localInfo);
startFastPollThread(remote, remoteInfo);
startFastPollThread(localAddress, localInfo);
startFastPollThread(remoteAddress, remoteInfo);
// Check local first
synchronized (localInfo) {
@@ -499,13 +506,13 @@ public class ComputerManagerService extends Service {
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
if (details.localAddress.equals(details.remoteAddress)) {
reachability = ComputerDetails.Reachability.REMOTE;
}
else {
// 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+")");
reachability = fastPollPc(details.localIp, details.remoteIp);
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +")");
reachability = fastPollPc(details.localAddress, details.remoteAddress);
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
@@ -517,39 +524,39 @@ public class ComputerManagerService extends Service {
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) {
polledDetails = tryPollIp(details, details.localIp);
polledDetails = tryPollIp(details, details.localAddress);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
polledDetails = tryPollIp(details, details.remoteAddress);
}
InetAddress reachableAddr = null;
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
String reachableAddr = null;
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details, details.localIp);
polledDetails = tryPollIp(details, details.localAddress);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
polledDetails = tryPollIp(details, details.remoteAddress);
}
if (polledDetails != null) {
// The fallback poll worked
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
}
}
else if (polledDetails != null) {
reachableAddr = localFirst ? details.localIp : details.remoteIp;
reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
}
if (reachableAddr == null) {
return null;
}
if (polledDetails.remoteIp.equals(reachableAddr)) {
if (polledDetails.remoteAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.localIp.equals(reachableAddr)) {
else if (polledDetails.localAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
@@ -571,7 +578,7 @@ public class ComputerManagerService extends Service {
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
if (confirmationReachTuple == null) {
// 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;
}
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 savedLocalAddress = details.localAddress;
String savedRemoteAddress = details.remoteAddress;
// If we got here, it's reachable
details.update(initialReachTuple.computer);
@@ -593,6 +603,33 @@ public class ComputerManagerService extends Service {
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;
}
@@ -616,7 +653,7 @@ public class ComputerManagerService extends Service {
for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add tuples for each computer
addTuple(computer);
addTuple(computer, true);
}
releaseLocalDatabaseReference();
@@ -694,8 +731,6 @@ public class ComputerManagerService extends Service {
public void run() {
int emptyAppListResponses = 0;
do {
InetAddress selectedAddr;
// Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) {
if (listener != null) {
@@ -709,19 +744,12 @@ public class ComputerManagerService extends Service {
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);
try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
String appList;
if (tuple != null) {
// If we're polling this machine too, grab the network lock
@@ -816,10 +844,10 @@ class PollingTuple {
}
class ReachabilityTuple {
public final InetAddress reachableAddress;
public final String reachableAddress;
public final ComputerDetails computer;
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
this.computer = computer;
this.reachableAddress = reachableAddress;
}
@@ -55,10 +55,10 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
convertView = inflater.inflate(layoutId, viewGroup, false);
}
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner);
ImageView imgView = convertView.findViewById(R.id.grid_image);
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
TextView txtView = convertView.findViewById(R.id.grid_text);
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
if (imgView != null) {
if (!populateImageView(imgView, prgView, itemList.get(i))) {
@@ -4,12 +4,11 @@ import android.content.Context;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.utils.ServerHelper;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
public class NetworkAssetLoader {
private final Context context;
@@ -21,10 +20,9 @@ public class NetworkAssetLoader {
}
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
InputStream in = null;
try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {}
@@ -37,13 +35,4 @@ public class NetworkAssetLoader {
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;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
@@ -17,7 +14,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
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),
getResources().getString(R.string.msg_add_pc), false);
try {
InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){
msg = getResources().getString(R.string.addpc_fail);
}
else {
msg = getResources().getString(R.string.addpc_success);
finish = true;
}
} catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host);
if (!managerBinder.addComputerBlocking(host, true)){
msg = getResources().getString(R.string.addpc_fail);
}
else {
msg = getResources().getString(R.string.addpc_success);
finish = true;
}
dialog.dismiss();
@@ -142,7 +132,7 @@ public class AddComputerManually extends Activity {
UiHelper.notifyNewRootView(this);
this.hostText = (TextView) findViewById(R.id.hostTextView);
this.hostText = findViewById(R.id.hostTextView);
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
@@ -3,7 +3,6 @@ package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Build;
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 VIDEO_FORMAT_PREF_STRING = "video_format";
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_60 = 10;
@@ -45,6 +45,7 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto";
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 AUTOSELECT_H265 = 0;
@@ -58,6 +59,7 @@ public class PreferenceConfiguration {
public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean onscreenController;
public boolean batterySaver;
public static int getDefaultBitrate(String resFpsString) {
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) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
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.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
return config;
}
@@ -2,7 +2,6 @@ package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.app.Activity;
import android.preference.Preference;
@@ -15,8 +14,6 @@ import com.limelight.PcView;
import com.limelight.R;
import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs;
@@ -3,6 +3,6 @@ package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView);
int getAdapterFragmentLayoutId();
void receiveAbsListView(AbsListView gridView);
}
@@ -1,5 +1,5 @@
package com.limelight.ui;
public interface GameGestures {
public void showKeyboard();
void showKeyboard();
}
@@ -1,6 +1,5 @@
package com.limelight.utils;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -14,21 +14,18 @@ import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ServerHelper {
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
computer.localAddress : computer.remoteAddress;
}
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
@@ -45,7 +42,7 @@ public class ServerHelper {
}
public static void doQuit(final Activity parent,
final InetAddress address,
final String address,
final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 14 KiB

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

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#565C64</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_pc_scut_background">#FFFFFF</color>
</resources>
+6
View File
@@ -47,6 +47,10 @@
<string name="error_404">GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU.
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
</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 -->
<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_disable_warnings">Disable warning messages</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="title_checkbox_51_surround">Enable 5.1 surround sound</string>
+3 -3
View File
@@ -21,9 +21,9 @@
android:title="@string/title_checkbox_stretch_video"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_disable_warnings"
android:title="@string/title_checkbox_disable_warnings"
android:summary="@string/summary_checkbox_disable_warnings"
android:key="checkbox_battery_saver"
android:title="@string/title_checkbox_battery_saver"
android:summary="@string/summary_checkbox_battery_saver"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_audio_settings">
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.2'
classpath 'com.android.tools.build:gradle:3.0.0-beta5'
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
#Thu Mar 02 18:32:01 PST 2017
#Sun Sep 03 12:51:12 PDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

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

After

Width:  |  Height:  |  Size: 3.4 KiB