Compare commits

...

111 Commits

Author SHA1 Message Date
Cameron Gutman 36f132942f Version 5.6.4 2018-01-20 15:13:17 -08:00
Cameron Gutman e4c251e7ee Ignore NVIDIA mouse capture extension on root builds to avoid broken LineageOS implementation 2018-01-20 02:31:40 -08:00
Cameron Gutman fb54bd5c78 Send the initial number of connected gamepads during launch to fix some games like L4D2 2018-01-20 01:16:25 -08:00
Cameron Gutman 8d4c86e113 Update common to support sending initial gamepads and fixing WoL 2018-01-20 01:11:39 -08:00
Cameron Gutman 7fafb8e0ff Revert extractNativeLibraries=false change due to install failure on Fire TV 3 2018-01-11 00:03:03 -08:00
Cameron Gutman fbcbe09255 Version 5.6.3 2018-01-10 00:40:08 -08:00
Cameron Gutman e336a4446a Update common to work with GFE 3.12 2018-01-10 00:38:15 -08:00
Cameron Gutman ffb35b2cdd Use smaller packets for streaming at 1080p and below to attempt to mitigate some reported regressions with v5.6.2 2018-01-09 23:38:25 -08:00
Cameron Gutman 2d0af6281c Ensure polling threads terminate even when polling resumes immediately 2017-12-29 14:05:29 -08:00
Cameron Gutman 472a7f6c8a Version 5.6.2 2017-12-27 22:45:07 -08:00
Cameron Gutman cd06559c66 Also count link-local addresses as local 2017-12-27 22:41:21 -08:00
Cameron Gutman d833933aaa Allow up to 1 second for fast poll to address connection flakiness 2017-12-27 22:27:35 -08:00
Cameron Gutman dc3495d59b Improve local vs. remote heuristics 2017-12-27 21:43:12 -08:00
Cameron Gutman e3a2e40043 Shrink large box art down to the normal size by changing sample size 2017-12-27 21:28:38 -08:00
Cameron Gutman 31e1fb743e Update common to address some null PC name crashes 2017-12-27 20:36:07 -08:00
Cameron Gutman bc59f11096 Disable RFI on b3_att_us 2017-12-27 20:34:02 -08:00
Cameron Gutman 6d97775aa9 Try disabling RFI if the previous run crashes 2017-12-27 20:32:34 -08:00
Cameron Gutman 3fff34e08a Don't extract native libraries for non-root build 2017-12-27 19:40:49 -08:00
Cameron Gutman 15e856dccb Move AudioTrack flush to cleanup() callback since all sample submission has ceased by then 2017-12-06 20:43:58 -08:00
Cameron Gutman 07d04171c3 Force HEVC enabled if HDR is requested 2017-12-05 17:38:25 -08:00
Cameron Gutman 42bd93cb3a Update common to fix mDNS race condition 2017-12-05 17:33:55 -08:00
Cameron Gutman 7d289f1134 Fix race conditions when frames are submitted after stop() has been called 2017-12-05 17:28:04 -08:00
Cameron Gutman 214461e123 Version 5.6.1 2017-12-01 00:42:43 -08:00
Cameron Gutman b0144a3256 Update decoder-errata.txt with HEVC errata 2017-12-01 00:40:32 -08:00
Cameron Gutman 3171256c6e Remove EvdevCaptureProvider components from non-root build 2017-12-01 00:37:25 -08:00
Cameron Gutman 5c69f6716c Don't build evdev_reader for the non-root variant 2017-12-01 00:10:55 -08:00
Cameron Gutman 6264781539 Update common to get decoder compatibility fixes 2017-11-30 23:47:08 -08:00
Cameron Gutman 0225f534d0 Fix H.265 streaming issues with MediaTek Android TV devices 2017-11-29 20:27:33 -08:00
Cameron Gutman 284a31737e Catch input buffer too small 2017-11-28 19:33:34 -08:00
Cameron Gutman b37a2dea57 Fix help display on some Android TV devices 2017-11-25 15:08:22 -08:00
Cameron Gutman 5c865e7f36 Version 5.6 r4 2017-11-25 14:33:41 -08:00
Cameron Gutman 04d9aea8c8 Detect and report decoder hangs 2017-11-25 14:27:04 -08:00
Cameron Gutman b6f52db9c3 Fix crash when input events are received and no H.264 decoder is present 2017-11-25 13:35:46 -08:00
Cameron Gutman 99d2e40683 Reset HDR when decoder crashes 3 times in a row 2017-11-25 13:21:04 -08:00
Cameron Gutman 02c4ed2724 Improve decoder crash reporting reliability 2017-11-25 13:19:30 -08:00
Cameron Gutman 5f4aab8f94 Improve decoder crash reporting detail 2017-11-25 12:56:54 -08:00
Cameron Gutman ec65901003 Report frames rendered in decoder crash report 2017-11-25 11:25:04 -08:00
Cameron Gutman 915acee88d Version 5.6 r3 2017-11-23 11:41:20 -08:00
Cameron Gutman 300d444f71 Ensure inForeground is set before CMS binding can complete 2017-11-23 11:34:22 -08:00
Cameron Gutman f37ab40c2f Fix race condition between completeOnCreate() and onConfigurationChanged() 2017-11-23 11:25:51 -08:00
Cameron Gutman 16e285d926 Version 5.6 r2 2017-11-21 21:33:06 -08:00
Cameron Gutman f2d122a275 Fix screen dimensions for portrait devices 2017-11-21 20:18:28 -08:00
Cameron Gutman bfa5a6349e Ensure MediaCodecHelper is initialized before evaluating codecs 2017-11-21 19:27:08 -08:00
Cameron Gutman a56689aea3 Always include resolutions that fit on the display 2017-11-21 19:18:41 -08:00
Cameron Gutman 3a5ba820cb Version 5.6 2017-11-20 23:08:43 -08:00
Cameron Gutman ec69fef36f Ignore back button presses on the default context 2017-11-20 22:46:57 -08:00
Cameron Gutman ff38074f55 Report GL Renderer in RendererException 2017-11-20 22:38:22 -08:00
Cameron Gutman 85d0ce0c40 Update Gradle to 3.0.1 2017-11-20 22:28:54 -08:00
Cameron Gutman 777129ca90 Move GLRenderer fetching into PcView to avoid race conditions inside Game activity and cache the result 2017-11-20 22:28:19 -08:00
Cameron Gutman 06156c4d68 Ignore back from goodix_fp device 2017-11-20 21:03:36 -08:00
Cameron Gutman 1c725b9dac Don't use reference picture invalidation on low-end Snapdragon SoCs 2017-11-20 20:56:31 -08:00
Cameron Gutman f761ee52db Exclude resolutions that are not supported by the decoders 2017-11-18 19:47:39 -08:00
Cameron Gutman 05e8cfcc0a Report adaptive playback status in crash reports 2017-11-18 18:31:12 -08:00
Cameron Gutman 912925ef2c Disable performance optimizations when in multi-window 2017-11-18 17:14:40 -08:00
Cameron Gutman 4deb881ec8 Enable adaptive playback on non-Intel devices 2017-11-18 16:37:17 -08:00
Cameron Gutman f55d6308ce Pass source rect to PiP to smoothly animate to 16:9 2017-11-18 16:29:03 -08:00
Cameron Gutman 44a3a141c0 Submit H.264 CSD in a single blob to try to prevent some decoder crashes 2017-11-18 15:14:25 -08:00
Cameron Gutman 37b5ba004c Fix IDR frame NALU drop race condition 2017-11-18 14:43:04 -08:00
Cameron Gutman b774b47213 Update for NDK 16 (deprecating MIPS) 2017-11-18 13:38:45 -08:00
Cameron Gutman 74dc00445e Version 5.5 2017-11-10 01:19:23 -08:00
Cameron Gutman 3b4563d5ea Suppress digital trigger events if an analog trigger axis is present. Fixes #465 2017-11-10 00:50:02 -08:00
Cameron Gutman 38669817b4 Update common to fix HEVC artifacts in some apps 2017-11-10 00:10:21 -08:00
Cameron Gutman 8f1d3ae04e Add support for PiP on Oreo 2017-11-09 23:28:22 -08:00
Cameron Gutman 74ed95871b Exclude HDR toggle when the device doesn't support it 2017-11-09 21:57:33 -08:00
Cameron Gutman cc5d67616c Prevent false USB access prompts due to races with kernel input stack bringup 2017-11-09 21:14:10 -08:00
Cameron Gutman eed7f09e6f Fix numpad operator keys not working 2017-11-07 22:03:40 -08:00
Cameron Gutman e3c1d23744 Fix SHIELD remote back button not working 2017-11-07 21:45:07 -08:00
Cameron Gutman c4b1200b43 Update build tools to 27.0.1 2017-11-07 21:44:27 -08:00
Cameron Gutman dff09f33a3 Fix shift not working on soft keyboard 2017-11-07 00:27:27 -08:00
Cameron Gutman 1f6b1dc2fe Send different VK codes for left and right ctrl/alt/shift keys. Fixes #318 2017-11-06 23:38:48 -08:00
Cameron Gutman 3f118dae93 Add HDR support and tweak HEVC supported decoders 2017-11-05 19:31:05 -08:00
Cameron Gutman 91a30ff6fe Target O MR1 2017-11-05 15:43:11 -08:00
BryanHaley 5102669b06 Virtual L3 R3 Buttons (#453)
* Added virtual L3 R3 options to better support gamepads missing these buttons.

* Update preferences.xml
2017-11-05 13:57:02 -08:00
Cameron Gutman 2e2f09be00 Fix frame drops when stopping the stream 2017-11-05 13:49:06 -08:00
Cameron Gutman c402103fe3 Avoid colliding with System UI in multi-window mode 2017-11-05 13:15:06 -08:00
Cameron Gutman 5e5df8abc8 Add never drop frames option for devices with micro-stuttering issues 2017-11-05 12:29:33 -08:00
Cameron Gutman d125eb7b16 Update to gradle 3.0.0 2017-11-05 12:08:16 -08:00
Cameron Gutman a116858493 Add .debug suffix to debug builds 2017-11-05 12:07:52 -08:00
Cameron Gutman 5f3b333e98 Version 5.2.1 2017-10-17 00:38:59 -07:00
Cameron Gutman 80a37855c7 Merge branch 'master' of github.com:moonlight-stream/moonlight-android 2017-10-17 00:37:00 -07:00
Cameron Gutman 5db1ec8ec0 Fix support for GFE 3.10 2017-10-17 00:35:36 -07:00
Cameron Gutman 8911c58e50 Block OMX.ffmpeg software decoders 2017-10-17 00:31:26 -07:00
Cameron Gutman 780a64694d Fix NPE when input device is removed during enumeration 2017-10-17 00:07:51 -07:00
Cameron Gutman 3c5ea9c8c3 Remove Nvidia's HEVC decoder from the hard blacklist now that it seems to be fine on Foster NRD90M 2017-10-08 22:06:06 -07:00
Cameron Gutman 40d1436ce3 Update for AS 3.0 Beta 7 2017-10-04 19:30:36 -07:00
Cameron Gutman dbb02acd37 Reintroduce the 75% HEVC bitrate multiplier that the old streaming core had 2017-09-25 21:39:53 -07:00
Cameron Gutman 20c4eac4ef Force HEVC disabled on Qualcomm SoCs older than Snapdragon 805 2017-09-19 21:21:23 -07:00
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
76 changed files with 1848 additions and 785 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 -13
View File
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion '26.0.1'
compileSdkVersion 27
buildToolsVersion '27.0.3'
defaultConfig {
minSdkVersion 16
targetSdkVersion 26
targetSdkVersion 27
versionName "5.1.2"
versionCode = 130
versionName "5.6.4"
versionCode = 143
}
flavorDimensions "root"
@@ -23,20 +23,24 @@ android {
// 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"
externalNativeBuild {
ndkBuild {
arguments "PRODUCT_FLAVOR=root"
}
}
applicationId "com.limelight.root"
dimension "root"
}
nonRoot {
applicationId "com.limelight"
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
externalNativeBuild {
ndkBuild {
arguments "PRODUCT_FLAVOR=nonRoot"
}
}
applicationId "com.limelight"
dimension "root"
}
}
@@ -46,6 +50,9 @@ android {
}
buildTypes {
debug {
applicationIdSuffix ".debug"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt')
@@ -60,8 +67,8 @@ android {
}
dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.52'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.52'
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')
+10 -1
View File
@@ -24,13 +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:appCategory="game"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
<!-- Samsung multi-window support -->
@@ -97,7 +103,10 @@
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:screenOrientation="sensorLandscape"
android:noHistory="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:theme="@style/StreamTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
+8 -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;
@@ -237,6 +235,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Assume we're in the foreground when created to avoid a race
// between binding to CMS and onResume()
inForeground = true;
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
@@ -250,7 +252,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);
@@ -302,6 +304,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
protected void onResume() {
super.onResume();
// Display a decoder crash notification if we've returned after a crash
UiHelper.showDecoderCrashDialog(this);
inForeground = true;
startComputerUpdates();
}
@@ -91,7 +91,7 @@ public class AppViewShortcutTrampoline extends Activity {
// If a game is running, we'll make the stream the top level activity
if (details.runningGameId != 0) {
intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this,
new NvApp("app", details.runningGameId), details, managerBinder));
new NvApp("app", details.runningGameId, false), details, managerBinder));
}
// Now start the activities
+198 -48
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;
@@ -19,6 +20,7 @@ import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.nvstream.jni.MoonBridge;
import com.limelight.preferences.GlPreferences;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.ui.StreamView;
@@ -28,13 +30,17 @@ import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.app.Service;
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.graphics.Rect;
import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.net.ConnectivityManager;
@@ -44,6 +50,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Rational;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
@@ -81,6 +88,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private VirtualController virtualController;
private PreferenceConfiguration prefConfig;
private SharedPreferences tombstonePrefs;
private NvConnection conn;
private SpinnerDialog spinner;
@@ -97,6 +105,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private ShortcutHelper shortcutHelper;
private MediaCodecDecoderRenderer decoderRenderer;
private boolean reportedCrash;
private WifiManager.WifiLock wifiLock;
@@ -122,13 +131,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
public static final String EXTRA_STREAMING_REMOTE = "Remote";
public static final String EXTRA_PC_UUID = "UUID";
public static final String EXTRA_PC_NAME = "PcName";
public static final String EXTRA_APP_HDR = "HDR";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
// We don't want a title bar
@@ -165,9 +173,11 @@ 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);
@@ -186,7 +196,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Warn the user if they're on a metered connection
checkDataConnection();
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (connMgr.isActiveNetworkMetered()) {
displayTransientMessage(getResources().getString(R.string.conn_metered));
}
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
@@ -201,6 +214,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
@@ -208,41 +222,102 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper = new ShortcutHelper(this);
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
shortcutHelper.reportShortcutUsed(uuid);
// Initialize the MediaCodec helper before creating the decoder
MediaCodecHelper.initializeWithContext(this);
GlPreferences glPrefs = GlPreferences.readPreferences(this);
MediaCodecHelper.initialize(this, glPrefs.glRenderer);
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat, prefConfig.bitrate, prefConfig.batterySaver);
// Check if the user has enabled HDR
if (prefConfig.enableHdr) {
// Check if the app supports it
if (!willStreamHdr) {
Toast.makeText(this, "This game does not support HDR10", Toast.LENGTH_SHORT).show();
}
// It does, so start our HDR checklist
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// We already know the app supports HDR if willStreamHdr is set.
Display display = getWindowManager().getDefaultDisplay();
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
// We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false;
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
LimeLog.info("Display supports HDR10");
foundHdr10 = true;
}
}
if (!foundHdr10) {
// Nope, no HDR for us :(
willStreamHdr = false;
Toast.makeText(this, "Display does not support HDR10", Toast.LENGTH_LONG).show();
}
}
else {
Toast.makeText(this, "HDR requires Android 7.0 or later", Toast.LENGTH_LONG).show();
willStreamHdr = false;
}
}
else {
willStreamHdr = false;
}
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig,
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();
reportedCrash = true;
}
},
tombstonePrefs.getInt("CrashCount", 0),
connMgr.isActiveNetworkMetered(),
willStreamHdr,
glPrefs.glRenderer
);
// Don't stream HDR if the decoder can't support it
if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) {
willStreamHdr = false;
Toast.makeText(this, "Decoder does not support HEVC Main10HDR10", Toast.LENGTH_LONG).show();
}
// 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()) {
Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
}
if (!decoderRenderer.isAvcSupported()) {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
// If we can't find an AVC decoder, we can't proceed
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
"This device or ROM doesn't support hardware accelerated H.264 playback.", true);
return;
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
if (!prefConfig.multiController && gamepadMask != 0) {
// If any gamepads are present in non-MC mode, set only gamepad 1.
gamepadMask = 1;
}
if (prefConfig.onscreenController) {
// If we're using OSC, always set at least gamepad 1.
gamepadMask |= 1;
}
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(new NvApp(appName, appId))
.setApp(new NvApp(appName, appId, willStreamHdr))
.setBitrate(prefConfig.bitrate * 1000)
.setEnableSops(prefConfig.enableSops)
.enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(remote ? 1024 : 1292)
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292)
.setRemote(remote)
.setHevcBitratePercentageMultiplier(75)
.setHevcSupported(decoderRenderer.isHevcSupported())
.setEnableHdr(willStreamHdr)
.setAttachedGamepadMask(gamepadMask)
.setAudioConfiguration(prefConfig.enable51Surround ?
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
MoonBridge.AUDIO_CONFIGURATION_STEREO)
@@ -274,7 +349,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (prefConfig.onscreenController) {
// create virtual onscreen controller
virtualController = new VirtualController(conn,
(FrameLayout)findViewById(R.id.surfaceView).getParent(),
(FrameLayout)streamView.getParent(),
this);
virtualController.refreshLayout();
}
@@ -285,10 +360,39 @@ public class Game extends Activity implements SurfaceHolder.Callback,
usbDriverServiceConnection, Service.BIND_AUTO_CREATE);
}
if (!decoderRenderer.isAvcSupported()) {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
// If we can't find an AVC decoder, we can't proceed
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
"This device or ROM doesn't support hardware accelerated H.264 playback.", true);
return;
}
// The connection will be started when the surface gets created
streamView.getHolder().addCallback(this);
}
@Override
public void onUserLeaveHint() {
super.onUserLeaveHint();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (prefConfig.enablePip && connected) {
enterPictureInPictureMode(
new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
.setSourceRectHint(new Rect(
streamView.getLeft(), streamView.getTop(),
streamView.getRight(), streamView.getBottom()))
.build());
}
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
@@ -392,20 +496,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
private void checkDataConnection()
{
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (mgr.isActiveNetworkMetered()) {
displayTransientMessage(getResources().getString(R.string.conn_metered));
}
}
@SuppressLint("InlinedApi")
private final Runnable hideSystemUi = new Runnable() {
@Override
public void run() {
// In multi-window mode on N+, we need to drop our layout flags or we'll
// be drawing underneath the system UI.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode()) {
Game.this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
// Use immersive mode on 4.4+ or standard low profile on previous builds
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Game.this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
@@ -430,6 +532,34 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
@Override
@TargetApi(Build.VERSION_CODES.N)
public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
super.onMultiWindowModeChanged(isInMultiWindowMode);
// In multi-window, we don't want to use the full-screen layout
// flag. It will cause us to collide with the system UI.
// This function will also be called for PiP so we can cover
// that case here too.
if (isInMultiWindowMode) {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// Disable performance optimizations for foreground
getWindow().setSustainedPerformanceMode(false);
decoderRenderer.notifyVideoBackground();
}
else {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// Enable performance optimizations for foreground
getWindow().setSustainedPerformanceMode(true);
decoderRenderer.notifyVideoForeground();
}
// Correct the system UI visibility flags
hideSystemUi(50);
}
@Override
protected void onDestroy() {
super.onDestroy();
@@ -478,7 +608,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Add the video codec to the post-stream toast
if (message != null) {
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
message += " [H.265 HDR]";
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
message += " [H.265]";
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
@@ -489,6 +622,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
// Clear the tombstone count if we terminated normally
if (!reportedCrash && tombstonePrefs.getInt("CrashCount", 0) != 0) {
tombstonePrefs.edit()
.putInt("CrashCount", 0)
.putInt("LastNotifiedCrashCount", 0)
.apply();
}
}
finish();
@@ -509,19 +650,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
};
// Returns true if the key stroke was consumed
private boolean handleSpecialKeys(short translatedKey, boolean down) {
private boolean handleSpecialKeys(int androidKeyCode, boolean down) {
int modifierMask = 0;
// Mask off the high byte
translatedKey &= 0xff;
if (translatedKey == KeyboardTranslator.VK_CONTROL) {
if (androidKeyCode == KeyEvent.KEYCODE_CTRL_LEFT ||
androidKeyCode == KeyEvent.KEYCODE_CTRL_RIGHT) {
modifierMask = KeyboardPacket.MODIFIER_CTRL;
}
else if (translatedKey == KeyboardTranslator.VK_SHIFT) {
else if (androidKeyCode == KeyEvent.KEYCODE_SHIFT_LEFT ||
androidKeyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
modifierMask = KeyboardPacket.MODIFIER_SHIFT;
}
else if (translatedKey == KeyboardTranslator.VK_ALT) {
else if (androidKeyCode == KeyEvent.KEYCODE_ALT_LEFT ||
androidKeyCode == KeyEvent.KEYCODE_ALT_RIGHT) {
modifierMask = KeyboardPacket.MODIFIER_ALT;
}
@@ -533,7 +674,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Check if Ctrl+Shift+Z is pressed
if (translatedKey == KeyboardTranslator.VK_Z &&
if (androidKeyCode == KeyEvent.KEYCODE_Z &&
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) ==
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT))
{
@@ -596,9 +737,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
)) {
@@ -615,7 +755,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Let this method take duplicate key down events
if (handleSpecialKeys(translated, true)) {
if (handleSpecialKeys(keyCode, true)) {
return true;
}
@@ -624,7 +764,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return super.onKeyDown(keyCode, event);
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState());
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState(event));
}
return true;
@@ -638,9 +778,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
)) {
@@ -656,7 +795,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return super.onKeyUp(keyCode, event);
}
if (handleSpecialKeys(translated, false)) {
if (handleSpecialKeys(keyCode, false)) {
return true;
}
@@ -926,7 +1065,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private void stopConnection() {
if (connecting || connected) {
connecting = connected = false;
conn.stop();
// Stop may take a few hundred ms to do some network I/O to tell
// the server we're going away and clean up. Let it run in a separate
// thread to keep things smooth for the UI. Inside moonlight-common,
// we prevent another thread from starting a connection before and
// during the process of stopping this one.
new Thread() {
public void run() {
conn.stop();
}
}.start();
}
}
@@ -1085,7 +1234,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
public void keyboardEvent(boolean buttonDown, short keyCode) {
short keyMap = KeyboardTranslator.translate(keyCode);
if (keyMap != 0) {
if (handleSpecialKeys(keyMap, buttonDown)) {
// handleSpecialKeys() takes the Android keycode
if (handleSpecialKeys(keyCode, buttonDown)) {
return;
}
@@ -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()));
}
});
+74 -37
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;
@@ -18,6 +16,7 @@ import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.GlPreferences;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
@@ -34,6 +33,8 @@ import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
@@ -51,12 +52,15 @@ import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class PcView extends Activity implements AdapterFragmentCallbacks {
private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter;
private ShortcutHelper shortcutHelper;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling, inForeground;
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
@@ -86,11 +90,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
};
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Reinitialize views just in case orientation changed
initializeViews();
// Only reinitialize views if completeOnCreate() was called
// before this callback. If it was not, completeOnCreate() will
// handle initializing views with the config change accounted for.
// This is not prone to races because both callbacks are invoked
// in the main thread.
if (completeOnCreateCalled) {
// Reinitialize views just in case orientation changed
initializeViews();
}
}
private final static int APP_LIST_ID = 1;
@@ -110,9 +122,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 +150,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);
}
@@ -152,6 +164,52 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Assume we're in the foreground when created to avoid a race
// between binding to CMS and onResume()
inForeground = true;
// Create a GLSurfaceView to fetch GLRenderer unless we have
// a cached result already.
final GlPreferences glPrefs = GlPreferences.readPreferences(this);
if (!glPrefs.savedFingerprint.equals(Build.FINGERPRINT) || glPrefs.glRenderer.isEmpty()) {
GLSurfaceView surfaceView = new GLSurfaceView(this);
surfaceView.setRenderer(new GLSurfaceView.Renderer() {
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
// Save the GLRenderer string so we don't need to do this next time
glPrefs.glRenderer = gl10.glGetString(GL10.GL_RENDERER);
glPrefs.savedFingerprint = Build.FINGERPRINT;
glPrefs.writePreferences();
LimeLog.info("Fetched GL Renderer: " + glPrefs.glRenderer);
runOnUiThread(new Runnable() {
@Override
public void run() {
completeOnCreate();
}
});
}
@Override
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
}
@Override
public void onDrawFrame(GL10 gl10) {
}
});
setContentView(surfaceView);
}
else {
LimeLog.info("Cached GL Renderer: " + glPrefs.glRenderer);
completeOnCreate();
}
}
private void completeOnCreate() {
completeOnCreateCalled = true;
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
@@ -220,6 +278,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onResume() {
super.onResume();
// Display a decoder crash notification if we've returned after a crash
UiHelper.showDecoderCrashDialog(this);
inForeground = true;
startComputerUpdates();
}
@@ -306,19 +367,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 +492,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));
@@ -542,7 +579,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return true;
}
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder);
return true;
case QUIT_ID:
@@ -557,7 +594,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0), managerBinder, null);
new NvApp("app", 0, false), managerBinder, null);
}
}, null);
return true;
@@ -149,6 +149,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
break;
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
e.printStackTrace();
try {
if (track != null) {
track.release();
@@ -158,6 +159,11 @@ public class AndroidAudioRenderer implements AudioRenderer {
}
}
if (track == null) {
// Couldn't create any audio track for playback
return -2;
}
return 0;
}
@@ -170,14 +176,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
public void start() {}
@Override
public void stop() {
// Immediately drop all pending data
track.pause();
track.flush();
}
public void stop() {}
@Override
public void cleanup() {
// Immediately drop all pending data
track.pause();
track.flush();
track.release();
}
}
@@ -2,6 +2,8 @@ package com.limelight.binding.input;
import android.content.Context;
import android.hardware.input.InputManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.InputDevice;
@@ -12,6 +14,7 @@ import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
@@ -48,7 +51,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private boolean hasGameController;
private final boolean multiControllerEnabled;
private short currentControllers;
private short currentControllers, initialControllers;
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
this.activityContext = activityContext;
@@ -96,6 +99,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
defaultContext.assignedControllerNumber = true;
// Some devices (GPD XD) have a back button which sends input events
// with device ID == 0. This hits the default context which would normally
// consume these. Instead, let's ignore them since that's probably the
// most likely case.
defaultContext.ignoreBack = true;
// Get the initially attached set of gamepads. As each gamepad receives
// its initial InputEvent, we will move these from this set onto the
// currentControllers set which will allow them to properly unplug
// if they are removed.
initialControllers = getAttachedControllerMask(activityContext);
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@@ -133,8 +148,47 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
onInputDeviceAdded(deviceId);
}
public static short getAttachedControllerMask(Context context) {
int count = 0;
short mask = 0;
// Count all input devices that are gamepads
InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
for (int id : im.getInputDeviceIds()) {
InputDevice dev = im.getInputDevice(id);
if (dev == null) {
continue;
}
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0) {
LimeLog.info("Counting InputDevice: "+dev.getName());
mask |= 1 << count++;
}
}
// Count all USB devices that match our drivers
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
for (UsbDevice dev : usbManager.getDeviceList().values()) {
if (UsbDriverService.shouldClaimDevice(dev)) {
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
mask |= 1 << count++;
}
}
LimeLog.info("Enumerated "+count+" gamepads");
return mask;
}
private void releaseControllerNumber(GenericControllerContext context) {
// If this device sent data as a gamepad, zero the values before removing
// If we reserved a controller number, remove that reservation
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
// If this device sent data as a gamepad, zero the values before removing.
// We must do this after clearing the currentControllers entry so this
// causes the device to be removed on the server PC.
if (context.assignedControllerNumber) {
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
(short) 0,
@@ -142,12 +196,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
(short) 0, (short) 0,
(short) 0, (short) 0);
}
// If we reserved a controller number, remove that reservation
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
}
// Called before sending input but after we've determined that this
@@ -175,6 +223,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
// Take this value out of the initial gamepad set
initialControllers &= ~(1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
@@ -195,6 +247,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
// Take this value out of the initial gamepad set
initialControllers &= ~(1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
@@ -375,7 +431,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.triggerDeadzone = 0.30f;
}
// Classify this device as a remote by name
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
else if (devName.toLowerCase().contains("remote")) {
// It's only a remote if it doesn't any sticks
if (!context.hasJoystickAxes) {
context.ignoreBack = true;
@@ -387,8 +443,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.rightStickDeadzoneRadius = 0.07f;
}
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
// back presses on this device
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey")) {
// back presses on this device. The Goodix buttons on the Nokia 6 also appear
// non-virtual so we'll ignore those too.
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey") ||
devName.equals("goodix_fp")) {
context.ignoreBack = true;
}
// The Serval has a couple of unknown buttons that are start and select. It also has
@@ -465,7 +523,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private short getActiveControllerMask() {
if (multiControllerEnabled) {
return currentControllers;
return (short)(currentControllers | initialControllers);
}
else {
// Only Player 1 is active with multi-controller disabled
@@ -966,9 +1024,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
if (context.leftTriggerAxis >= 0) {
// Suppress this digital event if an analog trigger is present
return true;
}
context.leftTrigger = 0;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
if (context.rightTriggerAxis >= 0) {
// Suppress this digital event if an analog trigger is present
return true;
}
context.rightTrigger = 0;
break;
default:
@@ -1078,9 +1144,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
if (context.leftTriggerAxis >= 0) {
// Suppress this digital event if an analog trigger is present
return true;
}
context.leftTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
if (context.rightTriggerAxis >= 0) {
// Suppress this digital event if an analog trigger is present
return true;
}
context.rightTrigger = (byte)0xFF;
break;
default:
@@ -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
@@ -20,22 +18,15 @@ public class KeyboardTranslator {
public static final int VK_9 = 57;
public static final int VK_A = 65;
public static final int VK_Z = 90;
public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17;
public static final int VK_BACK_SPACE = 8;
public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112;
public static final int VK_PERIOD = 46;
public static final int VK_INSERT = 155;
public static final int VK_OPEN_BRACKET = 91;
public static final int VK_WINDOWS = 524;
public static final int VK_MINUS = 45;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144;
@@ -45,7 +36,6 @@ public class KeyboardTranslator {
public static final int VK_CLOSE_BRACKET = 93;
public static final int VK_SCROLL_LOCK = 145;
public static final int VK_SEMICOLON = 59;
public static final int VK_SHIFT = 16;
public static final int VK_SLASH = 47;
public static final int VK_SPACE = 32;
public static final int VK_PRINTSCREEN = 154;
@@ -66,10 +56,9 @@ public class KeyboardTranslator {
public static short translate(int keycode) {
int translated;
/* There seems to be no clean mapping between Android key codes
* and what Nvidia sends over the wire. If someone finds one,
* I'll happily delete this code :)
*/
// This is a poor man's mapping between Android key codes
// and Windows VK_* codes. For all defined VK_ codes, see:
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
if (keycode >= KeyEvent.KEYCODE_0 &&
keycode <= KeyEvent.KEYCODE_9) {
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
@@ -89,8 +78,11 @@ public class KeyboardTranslator {
else {
switch (keycode) {
case KeyEvent.KEYCODE_ALT_LEFT:
translated = 0xA4;
break;
case KeyEvent.KEYCODE_ALT_RIGHT:
translated = VK_ALT;
translated = 0xA5;
break;
case KeyEvent.KEYCODE_BACKSLASH:
@@ -110,8 +102,11 @@ public class KeyboardTranslator {
break;
case KeyEvent.KEYCODE_CTRL_LEFT:
translated = 0xA2;
break;
case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = VK_CONTROL;
translated = 0xA3;
break;
case KeyEvent.KEYCODE_DEL:
@@ -131,23 +126,25 @@ public class KeyboardTranslator {
break;
case KeyEvent.KEYCODE_FORWARD_DEL:
// Nvidia maps period to delete
translated = VK_PERIOD;
translated = 0x2e;
break;
case KeyEvent.KEYCODE_INSERT:
translated = -1;
translated = 0x2d;
break;
case KeyEvent.KEYCODE_LEFT_BRACKET:
translated = 0xdb;
break;
case KeyEvent.KEYCODE_META_LEFT:
case KeyEvent.KEYCODE_META_RIGHT:
translated = VK_WINDOWS;
translated = 0x5b;
break;
case KeyEvent.KEYCODE_META_RIGHT:
translated = 0x5c;
break;
case KeyEvent.KEYCODE_MINUS:
translated = 0xbd;
break;
@@ -189,8 +186,11 @@ public class KeyboardTranslator {
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
translated = 0xA0;
break;
case KeyEvent.KEYCODE_SHIFT_RIGHT:
translated = VK_SHIFT;
translated = 0xA1;
break;
case KeyEvent.KEYCODE_SLASH:
@@ -237,6 +237,26 @@ public class KeyboardTranslator {
case KeyEvent.KEYCODE_BREAK:
translated = VK_PAUSE;
break;
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
translated = 0x6F;
break;
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
translated = 0x6A;
break;
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
translated = 0x6D;
break;
case KeyEvent.KEYCODE_NUMPAD_ADD:
translated = 0x6B;
break;
case KeyEvent.KEYCODE_NUMPAD_DOT:
translated = 0x6E;
break;
default:
System.out.println("No key for "+keycode);
@@ -3,8 +3,9 @@ package com.limelight.binding.input.capture;
import android.app.Activity;
import com.limelight.LimeLog;
import com.limelight.LimelightBuildProps;
import com.limelight.R;
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
import com.limelight.binding.input.evdev.EvdevListener;
public class InputCaptureManager {
@@ -13,13 +14,15 @@ public class InputCaptureManager {
LimeLog.info("Using Android O+ native mouse capture");
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
}
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity);
}
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
LimeLog.info("Using Evdev mouse capture");
return new EvdevCaptureProvider(activity, rootListener);
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
}
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
// Android N's native capture can't capture over system UI elements
@@ -11,6 +11,7 @@ import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.view.InputDevice;
@@ -72,10 +73,22 @@ public class UsbDriverService extends Service implements UsbDriverListener {
// Initial attachment broadcast
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// Continue the state machine
handleUsbDeviceState(device);
// shouldClaimDevice() looks at the kernel's enumerated input
// devices to make its decision about whether to prompt to take
// control of the device. The kernel bringing up the input stack
// may race with this callback and cause us to prompt when the
// kernel is capable of running the device. Let's post a delayed
// message to process this state change to allow the kernel
// some time to bring up the stack.
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// Continue the state machine
handleUsbDeviceState(device);
}
}, 1000);
}
// Subsequent permission dialog completion intent
else if (action.equals(ACTION_USB_PERMISSION)) {
@@ -144,13 +157,17 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
private boolean isRecognizedInputDevice(UsbDevice device) {
private static boolean isRecognizedInputDevice(UsbDevice device) {
// On KitKat and later, we can determine if this VID and PID combo
// matches an existing input device and defer to the built-in controller
// support in that case. Prior to KitKat, we'll always return true to be safe.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (int id : InputDevice.getDeviceIds()) {
InputDevice inputDev = InputDevice.getDevice(id);
if (inputDev == null) {
// Device was removed while looping
continue;
}
if (inputDev.getVendorId() == device.getVendorId() &&
inputDev.getProductId() == device.getProductId()) {
@@ -165,7 +182,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
private boolean shouldClaimDevice(UsbDevice device) {
public static boolean shouldClaimDevice(UsbDevice device) {
// We always bind to XB1 controllers but only bind to XB360 controllers
// if we know the kernel isn't already driving this device.
return XboxOneController.canClaimDevice(device) ||
@@ -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
};
@@ -0,0 +1,24 @@
package com.limelight.binding.input.evdev;
import android.app.Activity;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.input.capture.InputCaptureProvider;
public class EvdevCaptureProviderShim {
public static boolean isCaptureProviderSupported() {
return LimelightBuildProps.ROOT_BUILD;
}
// We need to construct our capture provider using reflection because it isn't included in non-root builds
public static InputCaptureProvider createEvdevCaptureProvider(Activity activity, EvdevListener listener) {
try {
Class providerClass = Class.forName("com.limelight.binding.input.evdev.EvdevCaptureProvider");
return (InputCaptureProvider) providerClass.getConstructors()[0].newInstance(activity, listener);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
@@ -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);
}
@@ -8,6 +8,7 @@ import android.content.Context;
import android.util.DisplayMetrics;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.preferences.PreferenceConfiguration;
public class VirtualControllerConfigurationLoader {
private static final String PROFILE_PATH = "profiles";
@@ -146,110 +147,130 @@ public class VirtualControllerConfigurationLoader {
public static void createDefaultLayout(final VirtualController controller, final Context context) {
DisplayMetrics screen = context.getResources().getDisplayMetrics();
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
// NOTE: Some of these getPercent() expressions seem like they can be combined
// into a single call. Due to floating point rounding, this isn't actually possible.
controller.addElement(createDigitalPad(controller, context),
getPercent(5, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(30, screen.widthPixels),
getPercent(40, screen.heightPixels)
);
if (!config.onlyL3R3)
{
controller.addElement(createDigitalPad(controller, context),
getPercent(5, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(30, screen.widthPixels),
getPercent(40, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftTrigger(
0, "LT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createRightTrigger(
0, "RT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftStick(controller, context),
getPercent(5, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createRightStick(controller, context),
getPercent(55, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
getPercent(40, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
}
controller.addElement(createDigitalButton(
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
getPercent(2, screen.widthPixels),
getPercent(80, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
getPercent(89, screen.widthPixels),
getPercent(80, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftTrigger(
0, "LT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createRightTrigger(
0, "RT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftStick(controller, context),
getPercent(5, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createRightStick(controller, context),
getPercent(55, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
getPercent(40, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
getPercent(40, screen.widthPixels)+getPercent(10, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
}
/*
@@ -0,0 +1,5 @@
package com.limelight.binding.video;
public interface CrashListener {
void notifyCrash(Exception e);
}
@@ -1,6 +1,7 @@
package com.limelight.binding.video;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Locale;
import org.jcodec.codecs.h264.H264Utils;
@@ -18,6 +19,7 @@ import android.media.MediaFormat;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CodecException;
import android.os.Build;
import android.util.Range;
import android.view.SurfaceHolder;
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@@ -31,9 +33,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private MediaCodecInfo avcDecoder;
private MediaCodecInfo hevcDecoder;
// Used for HEVC only
private byte[] vpsBuffer;
private byte[] spsBuffer;
private byte[] ppsBuffer;
private boolean submittedCsd;
private boolean submitCsdNextCall;
private MediaCodec videoDecoder;
private Thread rendererThread;
@@ -47,6 +51,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private int videoFormat;
private SurfaceHolder renderTarget;
private volatile boolean stopping;
private CrashListener crashListener;
private boolean reportedCrash;
private int consecutiveCrashCount;
private String glRenderer;
private boolean foreground = true;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
@@ -58,12 +67,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private long lastTimestampUs;
private long decoderTimeMs;
private long totalTimeMs;
private int totalFrames;
private int totalFramesReceived;
private int totalFramesRendered;
private int frameLossEvents;
private int framesLost;
private int lastFrameNumber;
private int refreshRate;
private int bitrate;
private PreferenceConfiguration prefs;
private int numSpsIn;
private int numPpsIn;
@@ -77,9 +87,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return decoder;
}
private MediaCodecInfo findHevcDecoder(int videoFormat) {
private MediaCodecInfo findHevcDecoder(PreferenceConfiguration prefs, boolean meteredNetwork, boolean requestedHdr) {
// Don't return anything if H.265 is forced off
if (videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
return null;
}
@@ -90,10 +100,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// for even required levels of HEVC.
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
if (decoderInfo != null) {
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName())) {
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork)) {
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
if (videoFormat == PreferenceConfiguration.FORCE_H265_ON) {
// HDR implies HEVC forced on, since HEVCMain10HDR10 is required for HDR
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_ON || requestedHdr) {
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
}
else {
@@ -109,13 +120,19 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
this.renderTarget = renderTarget;
}
public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver) {
public MediaCodecDecoderRenderer(PreferenceConfiguration prefs,
CrashListener crashListener, int consecutiveCrashCount,
boolean meteredData, boolean requestedHdr,
String glRenderer) {
//dumpDecoders();
this.bitrate = bitrate;
this.prefs = prefs;
this.crashListener = crashListener;
this.consecutiveCrashCount = consecutiveCrashCount;
this.glRenderer = glRenderer;
// Disable spinner threads in battery saver mode
if (batterySaver) {
// Disable spinner threads in battery saver mode or 4K
if (prefs.batterySaver || prefs.height >= 2160) {
spinnerThreads = new Thread[0];
}
else {
@@ -130,7 +147,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
LimeLog.warning("No AVC decoder found");
}
hevcDecoder = findHevcDecoder(videoFormat);
hevcDecoder = findHevcDecoder(prefs, meteredData, requestedHdr);
if (hevcDecoder != null) {
LimeLog.info("Selected HEVC decoder: "+hevcDecoder.getName());
}
@@ -145,10 +162,15 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// shared between AVC and HEVC decoders on the same device.
if (avcDecoder != null) {
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName());
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder.getName());
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName());
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder);
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
if (consecutiveCrashCount % 2 == 1) {
refFrameInvalidationAvc = refFrameInvalidationHevc = false;
LimeLog.warning("Disabling RFI due to previous crash");
}
if (directSubmit) {
LimeLog.info("Decoder "+avcDecoder.getName()+" will use direct submit");
}
@@ -169,6 +191,34 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return avcDecoder != null;
}
public boolean isHevcMain10Hdr10Supported() {
if (hevcDecoder == null) {
return false;
}
for (MediaCodecInfo.CodecProfileLevel profileLevel : hevcDecoder.getCapabilitiesForType("video/hevc").profileLevels) {
if (profileLevel.profile == MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10) {
LimeLog.info("HEVC decoder "+hevcDecoder.getName()+" supports HEVC Main10 HDR10");
return true;
}
}
return false;
}
public void notifyVideoForeground() {
startSpinnerThreads();
foreground = true;
}
public void notifyVideoBackground() {
// Signal the spinner threads to stop but
// don't wait for them to terminate to avoid
// delaying the state transition
signalSpinnerStop();
foreground = false;
}
public int getActiveVideoFormat() {
return this.videoFormat;
}
@@ -183,7 +233,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
String mimeType;
String selectedDecoderName;
if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
mimeType = "video/avc";
selectedDecoderName = avcDecoder.getName();
@@ -212,7 +262,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
refFrameInvalidationActive = refFrameInvalidationAvc;
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
mimeType = "video/hevc";
selectedDecoderName = hevcDecoder.getName();
@@ -317,6 +367,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// 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;
}
}
@@ -356,7 +410,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && prefs.disableFrameDrop) {
// Use a PTS that will cause this frame to never be dropped if frame dropping
// is disabled
videoDecoder.releaseOutputBuffer(lastIndex, 0);
}
else {
videoDecoder.releaseOutputBuffer(lastIndex, true);
}
totalFramesRendered++;
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
@@ -389,32 +452,76 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
rendererThread.start();
}
private void startSpinnerThread(final int i) {
spinnerThreads[i] = new Thread() {
@Override
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.
//
// Run until we notice our thread has been removed from the spinner threads
// array. Even if we don't notice immediately, we'll notice soon enough.
// This will also ensure we terminate even if someone has restarted spinning
// before we realized we should stop.
while (this == spinnerThreads[i]) {
try {
Thread.sleep(0, 1);
} catch (InterruptedException e) {
break;
}
}
}
};
spinnerThreads[i].setName("Spinner-"+i);
spinnerThreads[i].setPriority(Thread.MIN_PRIORITY);
spinnerThreads[i].start();
}
private void startSpinnerThreads() {
LimeLog.info("Using "+spinnerThreads.length+" spinner threads");
for (int i = 0; i < spinnerThreads.length; i++) {
spinnerThreads[i] = new Thread() {
@Override
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 (!stopping) {
try {
Thread.sleep(0, 1);
} catch (InterruptedException e) {
break;
}
}
}
};
spinnerThreads[i].setName("Spinner-"+i);
spinnerThreads[i].setPriority(Thread.MIN_PRIORITY);
spinnerThreads[i].start();
if (spinnerThreads[i] != null) {
continue;
}
startSpinnerThread(i);
}
}
private Thread[] signalSpinnerStop() {
// Capture current running threads
Thread[] runningThreads = Arrays.copyOf(spinnerThreads, spinnerThreads.length);
// Clear the spinner threads to signal their termination
for (int i = 0; i < spinnerThreads.length; i++) {
spinnerThreads[i] = null;
}
// Interrupt the threads
for (int i = 0; i < runningThreads.length; i++) {
if (runningThreads[i] != null) {
runningThreads[i].interrupt();
}
}
return runningThreads;
}
private void stopSpinnerThreads() {
// Signal and wait for the threads to stop
Thread[] runningThreads = signalSpinnerStop();
for (int i = 0; i < runningThreads.length; i++) {
if (runningThreads[i] != null) {
try {
runningThreads[i].join();
} catch (InterruptedException ignored) { }
}
}
}
private int dequeueInputBuffer() {
int index = -1;
long startTime, queueTime;
long startTime;
startTime = MediaCodecHelper.getMonotonicMillis();
@@ -427,14 +534,24 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return MediaCodec.INFO_TRY_AGAIN_LATER;
}
if (index < 0) {
return index;
int deltaMs = (int)(MediaCodecHelper.getMonotonicMillis() - startTime);
if (deltaMs >= 20) {
LimeLog.warning("Dequeue input buffer ran long: " + deltaMs + " ms");
}
queueTime = MediaCodecHelper.getMonotonicMillis();
if (queueTime - startTime >= 20) {
LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms");
if (index < 0) {
// We've been hung for 5 seconds and no other exception was reported,
// so generate a decoder hung exception
if (deltaMs >= 5000 && initialException == null) {
DecoderHungException decoderHungException = new DecoderHungException(deltaMs);
if (!reportedCrash) {
reportedCrash = true;
crashListener.notifyCrash(decoderHungException);
}
throw new RendererException(this, decoderHungException);
}
return index;
}
return index;
@@ -462,34 +579,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// 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();
}
for (Thread t : spinnerThreads) {
try {
t.join();
} catch (InterruptedException ignored) { }
}
stopSpinnerThreads();
}
@Override
@@ -551,8 +647,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@SuppressWarnings("deprecation")
@Override
public int submitDecodeUnit(byte[] frameData, int frameLength, int frameNumber, long receiveTimeMs) {
totalFrames++;
public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
int frameNumber, long receiveTimeMs) {
if (stopping) {
// Don't bother if we're stopping
return MoonBridge.DR_OK;
}
totalFramesReceived++;
// We can receive the same "frame" multiple times if it's an IDR frame.
// In that case, each frame start NALU is submitted independently.
@@ -582,14 +684,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
lastTimestampUs = timestampUs;
int codecFlags = 0;
boolean needsSpsReplay = false;
// H264 SPS
if (frameData[4] == 0x67) {
if (decodeUnitData[4] == 0x67) {
numSpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
ByteBuffer spsBuf = ByteBuffer.wrap(frameData);
ByteBuffer spsBuf = ByteBuffer.wrap(decodeUnitData);
// Skip to the start of the NALU data
spsBuf.position(5);
@@ -682,102 +782,74 @@ 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);
// The H264Utils.writeSPS function safely handles
// Annex B NALUs (including NALUs with escape sequences)
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, frameLength);
buf.put(escapedNalu);
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, decodeUnitLength);
if (queueInputBuffer(inputBufferIndex,
0, buf.position(),
timestampUs, codecFlags)) {
return MoonBridge.DR_OK;
}
else {
return MoonBridge.DR_NEED_IDR;
}
// H264 PPS
} else if (frameData[4] == 0x68) {
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");
needsBaselineSpsHack = false;
// Give the decoder the SPS again with the proper profile now
needsSpsReplay = true;
}
// Batch this to submit together with PPS
spsBuffer = new byte[5 + escapedNalu.limit()];
System.arraycopy(decodeUnitData, 0, spsBuffer, 0, 5);
escapedNalu.get(spsBuffer, 5, escapedNalu.limit());
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x40) {
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_VPS) {
numVpsIn++;
// Batch this to submit together with SPS and PPS per AOSP docs
vpsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, vpsBuffer, 0, frameLength);
vpsBuffer = new byte[decodeUnitLength];
System.arraycopy(decodeUnitData, 0, vpsBuffer, 0, decodeUnitLength);
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x42) {
// Only the HEVC SPS hits this path (H.264 is handled above)
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_SPS) {
numSpsIn++;
// Batch this to submit together with VPS and PPS per AOSP docs
spsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, spsBuffer, 0, frameLength);
spsBuffer = new byte[decodeUnitLength];
System.arraycopy(decodeUnitData, 0, spsBuffer, 0, decodeUnitLength);
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x44) {
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_PPS) {
numPpsIn++;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// If this is the first CSD blob or we aren't supporting
// adaptive playback, we will submit the CSD blob in a
// separate input buffer.
if (!submittedCsd || !adaptivePlayback) {
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;
}
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);
}
// When we get the PPS, submit the VPS and SPS together with
// the PPS, as required by AOSP docs on use of 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;
// This is the CSD blob
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
}
else {
// Batch this to submit together with the next I-frame
ppsBuffer = new byte[decodeUnitLength];
System.arraycopy(decodeUnitData, 0, ppsBuffer, 0, decodeUnitLength);
// Next call will be I-frame data
submitCsdNextCall = true;
return MoonBridge.DR_OK;
}
}
else {
inputBufferIndex = dequeueInputBuffer();
@@ -791,10 +863,34 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
if (submitCsdNextCall) {
if (vpsBuffer != null) {
buf.put(vpsBuffer);
}
if (spsBuffer != null) {
buf.put(spsBuffer);
}
if (ppsBuffer != null) {
buf.put(ppsBuffer);
}
submitCsdNextCall = false;
}
}
if (decodeUnitLength > buf.limit() - buf.position()) {
IllegalArgumentException exception = new IllegalArgumentException(
"Decode unit length "+decodeUnitLength+" too large for input buffer "+buf.limit());
if (!reportedCrash) {
reportedCrash = true;
crashListener.notifyCrash(exception);
}
throw new RendererException(this, exception);
}
// Copy data from our buffer list into the input buffer
buf.put(frameData, 0, frameLength);
buf.put(decodeUnitData, 0, decodeUnitLength);
if (!queueInputBuffer(inputBufferIndex,
0, buf.position(),
@@ -802,12 +898,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return MoonBridge.DR_NEED_IDR;
}
if (needsSpsReplay) {
if (!replaySps()) {
return MoonBridge.DR_NEED_IDR;
}
if ((codecFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
submittedCsd = true;
LimeLog.info("SPS replay complete");
if (needsBaselineSpsHack) {
needsBaselineSpsHack = false;
if (!replaySps()) {
return MoonBridge.DR_NEED_IDR;
}
LimeLog.info("SPS replay complete");
}
}
return MoonBridge.DR_OK;
@@ -872,53 +974,82 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
if (totalFramesReceived == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
return (int)(totalTimeMs / totalFramesReceived);
}
public int getAverageDecoderLatency() {
if (totalFrames == 0) {
if (totalFramesReceived == 0) {
return 0;
}
return (int)(decoderTimeMs / totalFrames);
return (int)(decoderTimeMs / totalFramesReceived);
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
static class DecoderHungException extends RuntimeException {
private int hangTimeMs;
private final Exception originalException;
private final MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer;
private int currentCodecFlags;
public RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.renderer = renderer;
this.originalException = e;
}
public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.renderer = renderer;
this.originalException = e;
this.currentBuffer = currentBuffer;
this.currentCodecFlags = currentCodecFlags;
DecoderHungException(int hangTimeMs) {
this.hangTimeMs = hangTimeMs;
}
public String toString() {
String str = "";
str += "Format: "+renderer.videoFormat+"\n";
str += "Hang time: "+hangTimeMs+" ms\n";
str += super.toString();
return str;
}
}
static class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
private String text;
RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.text = generateText(renderer, e, null, 0);
}
RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.text = generateText(renderer, e, currentBuffer, currentCodecFlags);
}
public String toString() {
return text;
}
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) {
String str = "";
str += "Format: "+String.format("%x", 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 += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
str += "AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper()+"\n";
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
str += "HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper()+"\n";
}
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
str += "GL Renderer: "+renderer.glRenderer+"\n";
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
str += "Foreground: "+renderer.foreground+"\n";
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.bitrate+" Mbps \n";
str += "Bitrate: "+renderer.prefs.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";
str += "Total frames received: "+renderer.totalFramesReceived+"\n";
str += "Total frames rendered: "+renderer.totalFramesRendered+"\n";
str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
if (currentBuffer != null) {
str += "Current buffer: ";
@@ -932,6 +1063,20 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (originalException instanceof CodecException) {
CodecException ce = (CodecException) originalException;
str += "Diagnostic Info: "+ce.getDiagnosticInfo()+"\n";
str += "Recoverable: "+ce.isRecoverable()+"\n";
str += "Transient: "+ce.isTransient()+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
str += "Codec Error Code: "+ce.getErrorCode()+"\n";
}
}
}
str += "/proc/cpuinfo:\n";
try {
str += MediaCodecHelper.readCpuinfo();
@@ -7,9 +7,10 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
@@ -27,7 +28,8 @@ public class MediaCodecHelper {
private static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
private static final List<String> deprioritizedHevcDecoders;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
private static final List<String> constrainedHighProfilePrefixes;
@@ -35,6 +37,9 @@ public class MediaCodecHelper {
private static final List<String> refFrameInvalidationAvcPrefixes;
private static final List<String> refFrameInvalidationHevcPrefixes;
private static boolean isLowEndSnapdragon = false;
private static boolean initialized = false;
static {
directSubmitPrefixes = new LinkedList<>();
@@ -71,10 +76,8 @@ public class MediaCodecHelper {
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");
// Never use ffmpeg decoders since they're software decoders
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
// Force these decoders disabled because:
// 1) They are software decoders, so the performance is terrible
@@ -94,11 +97,13 @@ public class MediaCodecHelper {
baselineProfileHackPrefixes = new LinkedList<>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
// if adaptive playback was enabled so let's avoid it to be safe.
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
// on some Android TV devices with H.265 only.
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
constrainedHighProfilePrefixes = new LinkedList<>();
constrainedHighProfilePrefixes.add("omx.intel");
@@ -115,8 +120,14 @@ public class MediaCodecHelper {
// Exynos seems to be the only HEVC decoder that works reliably
whitelistedHevcDecoders.add("omx.exynos");
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
//whitelistedHevcDecoders.add("omx.nvidia");
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
// For some reason, other X1 implementations require bitstream fixups.
if (Build.DEVICE.equalsIgnoreCase("darcy")) {
whitelistedHevcDecoders.add("omx.nvidia");
}
else {
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
}
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
@@ -134,20 +145,55 @@ public class MediaCodecHelper {
// during initialization to avoid SoCs with broken HEVC decoders.
}
public static void initializeWithContext(Context context) {
static {
deprioritizedHevcDecoders = new LinkedList<>();
// These are decoders that work but aren't used by default for various reasons.
// Qualcomm is currently the only decoders in this group.
}
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
glRenderer = glRenderer.toLowerCase().trim();
if (!glRenderer.contains("adreno")) {
return false;
}
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
Matcher matcher = modelNumberPattern.matcher(glRenderer);
if (!matcher.matches()) {
return false;
}
String modelNumber = matcher.group(2);
LimeLog.info("Found Adreno GPU: "+modelNumber);
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
return modelNumber.charAt(1) == '0';
}
public static void initialize(Context context, String glRenderer) {
if (initialized) {
return;
}
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
// Tegra K1 and later can do reference frame invalidation properly
if (configInfo.reqGlEsVersion >= 0x30000) {
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.qcom");
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.qcom");
// Prior to M, we were tricking the decoder into using baseline profile, which
// won't support RFI properly.
@@ -155,26 +201,36 @@ public class MediaCodecHelper {
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.intel");
}
}
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
// tell the good from the bad decoders are the generation of Adreno GPU included:
// 3xx - bad
// 4xx - good
//
// Unfortunately, it's not that easy to get that information here, so I'll use an
// approximation by checking the GLES level (<= 3.0 is bad).
if (configInfo.reqGlEsVersion > 0x30000) {
// FIXME: We prefer reference frame invalidation support (which is only doable on AVC on
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
// to force HEVC on.
//LimeLog.info("Added omx.qcom to supported HEVC decoders based on GLES 3.1+ support");
//whitelistedHevcDecoders.add("omx.qcom");
}
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
// tell the good from the bad decoders are the generation of Adreno GPU included:
// 3xx - bad
// 4xx - good
//
// Unfortunately, it's not that easy to get that information here, so I'll use an
// approximation by checking the GLES level (<= 3.0 is bad).
if (configInfo.reqGlEsVersion > 0x30000) {
// We prefer reference frame invalidation support (which is only doable on AVC on
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
// HEVC anyway.
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
deprioritizedHevcDecoders.add("omx.qcom");
}
else {
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
}
}
initialized = true;
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
if (!initialized) {
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
}
for (String badPrefix : decoderList) {
if (decoderName.length() >= badPrefix.length()) {
String prefix = decoderName.substring(0, badPrefix.length());
@@ -191,19 +247,14 @@ public class MediaCodecHelper {
return System.nanoTime() / 1000000L;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
/*
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
so we'll keep it off for now, since we don't know whether other devices also do the same
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)");
return true;
}
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
LimeLog.info("Decoder blacklisted for adaptive playback");
return false;
}
try {
if (decoderInfo.getCapabilitiesForType("video/avc").
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
@@ -215,7 +266,7 @@ public class MediaCodecHelper {
} catch (Exception e) {
// Tolerate buggy codecs
}
}*/
}
return false;
}
@@ -236,7 +287,18 @@ public class MediaCodecHelper {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName) {
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
if (videoHeight > 720 && isLowEndSnapdragon) {
return false;
}
// This device seems to crash constantly at 720p, so try disabling
// RFI to see if we can get that under control.
if (Build.PRODUCT.equalsIgnoreCase("b3_att_us")) {
return false;
}
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
}
@@ -244,7 +306,7 @@ public class MediaCodecHelper {
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
}
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
// TODO: Shield Tablet K1/LTE?
//
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
@@ -276,6 +338,15 @@ public class MediaCodecHelper {
return false;
}
// Some devices have HEVC decoders that we prefer not to use
// typically because it can't support reference frame invalidation.
// However, we will use it for HDR and for streaming over mobile networks
// since it works fine otherwise.
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
LimeLog.info("Selected deprioritized decoder");
return true;
}
return isDecoderInList(whitelistedHevcDecoders, decoderName);
}
@@ -323,6 +394,10 @@ public class MediaCodecHelper {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
if (!initialized) {
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
}
for (String preferredDecoder : preferredDecoders) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
@@ -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);
}
@@ -6,6 +6,7 @@ import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -20,6 +21,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;
@@ -35,7 +37,7 @@ public class ComputerManagerService extends Service {
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
private static final int FAST_POLL_TIMEOUT = 500;
private static final int FAST_POLL_TIMEOUT = 1000;
private static final int OFFLINE_POLL_TRIES = 5;
private static final int INITIAL_POLL_TRIES = 2;
private static final int EMPTY_LIST_THRESHOLD = 3;
@@ -132,7 +134,7 @@ public class ComputerManagerService extends Service {
public void run() {
int offlineCount = 0;
while (!isInterrupted() && pollingActive) {
while (!isInterrupted() && pollingActive && tuple.thread == this) {
try {
// Only allow one request to the machine at a time
synchronized (tuple.networkLock) {
@@ -154,7 +156,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 +212,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 +291,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 +307,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 +347,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,10 +365,14 @@ 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 {
if (!manuallyAdded) {
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
}
return false;
}
}
@@ -379,6 +392,7 @@ public class ComputerManagerService extends Service {
if (tuple.thread != null) {
// Interrupt the thread on this entry
tuple.thread.interrupt();
tuple.thread = null;
}
pollingTuples.remove(tuple);
break;
@@ -404,14 +418,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 +446,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 +457,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 +471,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) {
@@ -493,19 +507,31 @@ public class ComputerManagerService extends Service {
return ComputerDetails.Reachability.OFFLINE;
}
private static boolean isAddressLikelyLocal(String str) {
try {
// This will tend to be wrong for IPv6 but falling back to
// remote will be fine in that case. For IPv4, it should be
// pretty accurate due to NAT prevalence.
InetAddress addr = InetAddress.getByName(str);
return addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
return false;
}
}
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
reachability = ComputerDetails.Reachability.REMOTE;
if (details.localAddress.equals(details.remoteAddress)) {
reachability = isAddressLikelyLocal(details.localAddress) ?
ComputerDetails.Reachability.LOCAL : 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 +543,46 @@ 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 both addresses are the same, guess whether we're local based on
// IP address heuristics.
if (reachableAddr.equals(polledDetails.localAddress) &&
reachableAddr.equals(polledDetails.remoteAddress)) {
polledDetails.reachability = isAddressLikelyLocal(reachableAddr) ?
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
}
else 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 +604,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 +614,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 +629,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 +679,7 @@ public class ComputerManagerService extends Service {
for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add tuples for each computer
addTuple(computer);
addTuple(computer, true);
}
releaseLocalDatabaseReference();
@@ -694,8 +757,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 +770,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
@@ -787,6 +841,7 @@ public class ComputerManagerService extends Service {
} while (waitPollingDelay());
}
};
thread.setName("App list polling thread for " + computer.localAddress);
thread.start();
}
@@ -816,10 +871,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))) {
@@ -13,7 +13,11 @@ import java.io.OutputStream;
public class DiskAssetLoader {
// 5 MB
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
private static final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
// Standard box art is 300x400
private static final int STANDARD_ASSET_WIDTH = 300;
private static final int STANDARD_ASSET_HEIGHT = 400;
private final File cacheDir;
@@ -25,33 +29,65 @@ public class DiskAssetLoader {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null;
Bitmap bmp = null;
try {
// Make sure the cached asset doesn't exceed the maximum size
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
return null;
}
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (IOException ignored) {
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
// Don't bother with anything if it doesn't exist
if (!file.exists()) {
return null;
}
// Make sure the cached asset doesn't exceed the maximum size
if (file.length() > MAX_ASSET_SIZE) {
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
file.delete();
return null;
}
// Lookup bounds of the downloaded image
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
decodeOnlyOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
// Dimensions set to -1 on error. Return value always null.
return null;
}
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
// Load the image scaled to the appropriate size
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
STANDARD_ASSET_WIDTH / sampleSize,
STANDARD_ASSET_HEIGHT / sampleSize);
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inDither = true;
Bitmap bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bmp != null) {
LimeLog.info("Disk cache hit for tuple: "+tuple);
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
}
return bmp;
@@ -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
@@ -0,0 +1,37 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
public class GlPreferences {
private static final String PREF_NAME = "GlPreferences";
private static final String FINGERPRINT_PREF_STRING = "Fingerprint";
private static final String GL_RENDERER_PREF_STRING = "Renderer";
private SharedPreferences prefs;
public String glRenderer;
public String savedFingerprint;
private GlPreferences(SharedPreferences prefs) {
this.prefs = prefs;
}
public static GlPreferences readPreferences(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, 0);
GlPreferences glPrefs = new GlPreferences(prefs);
glPrefs.glRenderer = prefs.getString(GL_RENDERER_PREF_STRING, "");
glPrefs.savedFingerprint = prefs.getString(FINGERPRINT_PREF_STRING, "");
return glPrefs;
}
public boolean writePreferences() {
return prefs.edit()
.putString(GL_RENDERER_PREF_STRING, glRenderer)
.putString(FINGERPRINT_PREF_STRING, savedFingerprint)
.commit();
}
}
@@ -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,7 +22,11 @@ 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 ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -46,7 +49,11 @@ 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 ONLY_L3_R3_DEFAULT = false;
private static final boolean DEFAULT_BATTERY_SAVER = false;
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
private static final boolean DEFAULT_ENABLE_HDR = false;
private static final boolean DEFAULT_ENABLE_PIP = false;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
@@ -60,7 +67,11 @@ public class PreferenceConfiguration {
public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean onscreenController;
public boolean onlyL3R3;
public boolean batterySaver;
public boolean disableFrameDrop;
public boolean enableHdr;
public boolean enablePip;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
@@ -131,6 +142,17 @@ public class PreferenceConfiguration {
}
}
public static void resetStreamingSettings(Context context) {
// We consider resolution, FPS, bitrate, HDR, 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)
.remove(ENABLE_HDR_PREF_STRING)
.apply();
}
public static PreferenceConfiguration readPreferences(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration();
@@ -191,7 +213,11 @@ 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.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
return config;
}
@@ -2,21 +2,25 @@ package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.media.MediaCodecInfo;
import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.Range;
import android.view.Display;
import com.limelight.LimeLog;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs;
@@ -53,6 +57,37 @@ public class StreamSettings extends Activity {
}
public static class SettingsFragment extends PreferenceFragment {
private static void removeResolution(ListPreference pref, String prefix) {
int matchingCount = 0;
// Count the number of matching entries we'll be removing
for (CharSequence seq : pref.getEntryValues()) {
if (seq.toString().startsWith(prefix)) {
matchingCount++;
}
}
// Create the new arrays
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
int outIndex = 0;
for (int i = 0; i < pref.getEntryValues().length; i++) {
if (pref.getEntryValues()[i].toString().startsWith(prefix)) {
// Skip matching prefixes
continue;
}
entries[outIndex] = pref.getEntries()[i];
entryValues[outIndex] = pref.getEntryValues()[i];
outIndex++;
}
// Update the preference with the new list
pref.setEntries(entries);
pref.setEntryValues(entryValues);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -68,6 +103,130 @@ public class StreamSettings extends Activity {
screen.removePreference(category);
}
// Remove PiP mode on devices pre-Oreo
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
category.removePreference(findPreference("checkbox_enable_pip"));
}
// Hide non-supported resolution/FPS combinations
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display display = getActivity().getWindowManager().getDefaultDisplay();
int maxSupportedResW = 0;
// Always allow resolutions that are smaller or equal to the active
// display resolution because decoders can report total non-sense to us.
// For example, a p201 device reports:
// AVC Decoder: OMX.amlogic.avc.decoder.awesome
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
// AVC supported width range: 64 - 384
// HEVC supported width range: 64 - 544
for (Display.Mode candidate : display.getSupportedModes()) {
// Some devices report their dimensions in the portrait orientation
// where height > width. Normalize these to the conventional width > height
// arrangement before we process them.
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
}
// This must be called to do runtime initialization before calling functions that evaluate
// decoder lists.
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1);
MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
if (avcDecoder != null) {
Range<Integer> avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper());
// If 720p is not reported as supported, ignore all results from this API
if (avcWidthRange.contains(1280)) {
if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
else if (maxSupportedResW < 1280) {
maxSupportedResW = 1280;
}
}
}
if (hevcDecoder != null) {
Range<Integer> hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper());
// If 720p is not reported as supported, ignore all results from this API
if (hevcWidthRange.contains(1280)) {
if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
else if (maxSupportedResW < 1280) {
maxSupportedResW = 1280;
}
}
}
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
if (maxSupportedResW != 0) {
if (maxSupportedResW < 3840) {
// 4K is unsupported
removeResolution(resPref, "4K");
}
if (maxSupportedResW < 1920) {
// 1080p is unsupported
removeResolution(resPref, "1080p");
}
// Never remove 720p
}
}
// Remove HDR preference for devices below Nougat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
LimeLog.info("Excluding HDR toggle based on OS");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_enable_hdr"));
}
else {
Display display = getActivity().getWindowManager().getDefaultDisplay();
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
// We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false;
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
foundHdr10 = true;
}
}
if (!foundHdr10) {
LimeLog.info("Excluding HDR toggle based on display capabilities");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_enable_hdr"));
}
}
// Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
@@ -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();
}
@@ -13,7 +13,7 @@ import java.io.OutputStream;
import java.io.Reader;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
public static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
@@ -13,18 +13,18 @@ public class Dialog implements Runnable {
private final String title;
private final String message;
private final Activity activity;
private final boolean endAfterDismiss;
private final Runnable runOnDismiss;
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
private Dialog(Activity activity, String title, String message, Runnable runOnDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
this.runOnDismiss = runOnDismiss;
}
public static void closeDialogs()
@@ -40,9 +40,21 @@ public class Dialog implements Runnable {
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
public static void displayDialog(final Activity activity, String title, String message, final boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
activity.runOnUiThread(new Dialog(activity, title, message, new Runnable() {
@Override
public void run() {
if (endAfterDismiss) {
activity.finish();
}
}
}));
}
public static void displayDialog(Activity activity, String title, String message, Runnable runOnDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, runOnDismiss));
}
@Override
@@ -65,9 +77,7 @@ public class Dialog implements Runnable {
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
runOnDismiss.run();
}
});
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
@@ -77,9 +87,7 @@ public class Dialog implements Runnable {
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
runOnDismiss.run();
HelpLauncher.launchTroubleshooting(activity);
}
@@ -1,24 +1,43 @@
package com.limelight.utils;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import com.limelight.HelpActivity;
public class HelpLauncher {
private static boolean isKnownBrowser(Context context, Intent i) {
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
if (resolvedActivity == null) {
// No browser
return false;
}
String name = resolvedActivity.activityInfo.name;
if (name == null) {
return false;
}
name = name.toLowerCase();
return name.contains("chrome") || name.contains("firefox");
}
private static void launchUrl(Context context, String url) {
// Try to launch the default browser
try {
// Fire TV devices will lie and say they do have a browser
// even though the OS just shows an error dialog if we
// try to use it.
if (!"Amazon".equalsIgnoreCase(Build.MANUFACTURER)) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
// Several Android TV devices will lie and say they do have a browser
// even though the OS just shows an error dialog if we try to use it. We need to
// be a bit more clever on these devices and detect if the browser is a legitimate
// browser or just a fake error message activity.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
isKnownBrowser(context, i)) {
context.startActivity(i);
return;
}
@@ -14,23 +14,21 @@ 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_APP_HDR, app.isHdrSupported());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
@@ -45,7 +43,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) {
@@ -5,6 +5,7 @@ import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.view.View;
@@ -58,6 +59,44 @@ public class UiHelper {
}
}
public static void showDecoderCrashDialog(Activity activity) {
final SharedPreferences prefs = activity.getSharedPreferences("DecoderTombstone", 0);
final 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(activity);
Dialog.displayDialog(activity,
activity.getResources().getString(R.string.title_decoding_reset),
activity.getResources().getString(R.string.message_decoding_reset),
new Runnable() {
@Override
public void run() {
// Mark notification as acknowledged on dismissal
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
}
});
}
else {
Dialog.displayDialog(activity,
activity.getResources().getString(R.string.title_decoding_error),
activity.getResources().getString(R.string.message_decoding_error),
new Runnable() {
@Override
public void run() {
// Mark notification as acknowledged on dismissal
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
}
});
}
}
}
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
+1 -7
View File
@@ -1,10 +1,4 @@
# Application.mk for Limelight
# Application.mk for Moonlight
# Our minimum version is Android 4.1
APP_PLATFORM := android-16
# Support all modern ABIs
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips mips64
# We want an optimized build
APP_OPTIM := release
+22 -18
View File
@@ -5,28 +5,32 @@ include $(call all-subdir-makefiles)
LOCAL_PATH := $(MY_LOCAL_PATH)
include $(CLEAR_VARS)
LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
# Only build evdev_reader for the rooted APK flavor
ifeq (root,$(PRODUCT_FLAVOR))
include $(CLEAR_VARS)
LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
# This next portion of the makefile is mostly copied from build-executable.mk but
# creates a binary with the libXXX.so form so the APK will install and drop
# the binary correctly.
# This next portion of the makefile is mostly copied from build-executable.mk but
# creates a binary with the libXXX.so form so the APK will install and drop
# the binary correctly.
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
# we are building target objects
my := TARGET_
# we are building target objects
my := TARGET_
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
LOCAL_MODULE_CLASS := EXECUTABLE
include $(BUILD_SYSTEM)/build-module.mk
endif
LOCAL_MODULE_CLASS := EXECUTABLE
include $(BUILD_SYSTEM)/build-module.mk
Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 38 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>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_pc_scut_background"/>
<foreground android:drawable="@mipmap/ic_pc_scut_foreground"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

+2
View File
@@ -109,6 +109,8 @@
<string name="category_on_screen_controls_settings">Configuración de controles en pantalla</string>
<string name="title_checkbox_show_onscreen_controls">Mostrar controles en pantalla</string>
<string name="summary_checkbox_show_onscreen_controls">Muestra controles virtuales superpuestos en la pantalla táctil</string>
<string name="title_only_l3r3">Solo muestra L3 y R3</string>
<string name="summary_only_l3r3">Ocultar todo excepto L3 y R3</string>
<string name="category_ui_settings">Configuración de la interfaz</string>
<string name="title_language_list">Idioma</string>
+2
View File
@@ -120,6 +120,8 @@
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
<string name="title_only_l3r3">Montre seulement L3 et R3</string>
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
<string name="title_language_list">Langue</string>
+13 -1
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>
@@ -106,6 +110,8 @@
<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="title_checkbox_enable_pip">Enable Picture-in-Picture observer mode</string>
<string name="summary_checkbox_enable_pip">Allows the stream to be viewed (but not controlled) while multitasking</string>
<string name="category_audio_settings">Audio Settings</string>
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
@@ -122,6 +128,8 @@
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
<string name="summary_checkbox_show_onscreen_controls">Show virtual controller overlay on touchscreen</string>
<string name="title_only_l3r3">Only show L3 and R3</string>
<string name="summary_only_l3r3">Hide all virtual buttons except L3 and R3</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
@@ -138,7 +146,11 @@
<string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
<string name="category_advanced_settings">Advanced Settings</string>
<string name="title_disable_frame_drop">Never drop frames</string>
<string name="summary_disable_frame_drop">May reduce micro-stuttering on some devices, but can increase latency</string>
<string name="title_video_format">Change H.265 settings</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device.</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device</string>
<string name="title_enable_hdr">Enable HDR (Experimental)</string>
<string name="summary_enable_hdr">Stream HDR when the game and PC GPU support it. HDR requires a GTX 1000 series GPU or later.</string>
</resources>
+28 -4
View File
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/category_basic_settings">
<PreferenceCategory android:title="@string/category_basic_settings"
android:key="category_basic_settings">
<ListPreference
android:key="list_resolution_fps"
android:title="@string/title_resolution_list"
@@ -25,6 +26,11 @@
android:title="@string/title_checkbox_battery_saver"
android:summary="@string/summary_checkbox_battery_saver"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_enable_pip"
android:title="@string/title_checkbox_enable_pip"
android:summary="@string/summary_checkbox_enable_pip"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_audio_settings">
<CheckBoxPreference
@@ -54,10 +60,17 @@
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
android:key="category_onscreen_controls">
<CheckBoxPreference
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:defaultValue="false"
android:key="checkbox_show_onscreen_controls"
android:title="@string/title_checkbox_show_onscreen_controls"
android:summary="@string/summary_checkbox_show_onscreen_controls"
android:defaultValue="false"/>
android:title="@string/title_checkbox_show_onscreen_controls" />
<CheckBoxPreference
android:defaultValue="false"
android:key="checkbox_only_show_L3R3"
android:summary="@string/summary_only_l3r3"
android:title="@string/title_only_l3r3" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference
@@ -89,7 +102,8 @@
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings">
<PreferenceCategory android:title="@string/category_advanced_settings"
android:key="category_advanced_settings">
<ListPreference
android:key="video_format"
android:title="@string/title_video_format"
@@ -97,5 +111,15 @@
android:entryValues="@array/video_format_values"
android:summary="@string/summary_video_format"
android:defaultValue="auto" />
<CheckBoxPreference
android:key="checkbox_disable_frame_drop"
android:title="@string/title_disable_frame_drop"
android:summary="@string/summary_disable_frame_drop"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_enable_hdr"
android:title="@string/title_enable_hdr"
android:summary="@string/summary_enable_hdr"
android:defaultValue="false" />
</PreferenceCategory>
</PreferenceScreen>
+1
View File
@@ -2,5 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Non-root application name -->
<!-- FIXME: We should set extractNativeLibs=false but this breaks installation on the Fire TV 3 -->
<application android:label="Moonlight" />
</manifest>
+5 -1
View File
@@ -2,5 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Root application name -->
<application android:label="Moonlight (Root)" />
<!-- Ensure native libraries are always extracted for root builds,
since we must invoke the evdev_reader binary ourselves -->
<application
android:label="Moonlight (Root)"
android:extractNativeLibs="true" />
</manifest>
@@ -6,7 +6,6 @@ import android.os.Looper;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.input.capture.InputCaptureProvider;
import java.io.DataOutputStream;
@@ -186,10 +185,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
}
public static boolean isCaptureProviderSupported() {
return LimelightBuildProps.ROOT_BUILD;
}
private void reportDeviceNotRooted() {
activity.runOnUiThread(new Runnable() {
@Override
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-beta2'
classpath 'com.android.tools.build:gradle:3.0.1'
}
}
+13 -1
View File
@@ -28,4 +28,16 @@ This file serves to document some of the decoder errata when using MediaCodec ha
- Affected decoders: Intel decoder in Nexus Player (after Android 6.0)
10. Some decoders actually suffer increased latency when max_dec_frame_buffering=1
- Affected decoders: MediaTek decoder in Fire TV 2015
- Affected decoders: MediaTek decoder in Fire TV 2015
11. Attempting to use reference picture invalidation at 1080p causes the decoder to crash on low-end Snapdragon SoCs. 720p is unaffected.
- Affected decoders: Snapdragon 200, 410, 415, 430, 435, 616
12. Enabling adaptive playback causes H.265 1080p and 4K playback to fail on some MediaTek SoCs.
- Affected decoders: MT5832 in Sony BRAVIA 4K GB (BRAVIA_ATV2) and MT5890 in Phillips 55PUS6501
13. Some HEVC decoders hang when receiving a stream with 16 reference frames
- Affected decoders: Amlogic S905Z in Fire TV 3
14. Some HEVC decoders lag when receiving a stream with 16 reference frames
- Affected decoders: Tegra X1 in Pixel C (but NOT in SHIELD TV darcy)
+2 -2
View File
@@ -1,6 +1,6 @@
#Sat Aug 12 15:52:56 PDT 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-4.1-rc-1-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