mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-02-16 18:40:46 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88f84a0c12 | ||
|
|
03ecf3e5ac | ||
|
|
617c8582b4 | ||
|
|
ef3b28295b | ||
|
|
3bcd2ee068 | ||
|
|
d4ff58b3ad | ||
|
|
c797318ece | ||
|
|
82387d23f8 | ||
|
|
766e629be5 | ||
|
|
b93aa42c0c | ||
|
|
36f132942f | ||
|
|
e4c251e7ee | ||
|
|
fb54bd5c78 | ||
|
|
8d4c86e113 | ||
|
|
7fafb8e0ff | ||
|
|
fbcbe09255 | ||
|
|
e336a4446a | ||
|
|
ffb35b2cdd | ||
|
|
2d0af6281c | ||
|
|
472a7f6c8a | ||
|
|
cd06559c66 | ||
|
|
d833933aaa | ||
|
|
dc3495d59b | ||
|
|
e3a2e40043 | ||
|
|
31e1fb743e | ||
|
|
bc59f11096 | ||
|
|
6d97775aa9 | ||
|
|
3fff34e08a | ||
|
|
15e856dccb | ||
|
|
07d04171c3 | ||
|
|
42bd93cb3a | ||
|
|
7d289f1134 | ||
|
|
214461e123 | ||
|
|
b0144a3256 | ||
|
|
3171256c6e | ||
|
|
5c69f6716c | ||
|
|
6264781539 | ||
|
|
0225f534d0 | ||
|
|
284a31737e | ||
|
|
b37a2dea57 | ||
|
|
5c865e7f36 | ||
|
|
04d9aea8c8 | ||
|
|
b6f52db9c3 | ||
|
|
99d2e40683 | ||
|
|
02c4ed2724 | ||
|
|
5f4aab8f94 | ||
|
|
ec65901003 | ||
|
|
915acee88d | ||
|
|
300d444f71 | ||
|
|
f37ab40c2f | ||
|
|
16e285d926 | ||
|
|
f2d122a275 | ||
|
|
bfa5a6349e | ||
|
|
a56689aea3 | ||
|
|
3a5ba820cb | ||
|
|
ec69fef36f | ||
|
|
ff38074f55 | ||
|
|
85d0ce0c40 | ||
|
|
777129ca90 | ||
|
|
06156c4d68 | ||
|
|
1c725b9dac | ||
|
|
f761ee52db | ||
|
|
05e8cfcc0a | ||
|
|
912925ef2c | ||
|
|
4deb881ec8 | ||
|
|
f55d6308ce | ||
|
|
44a3a141c0 | ||
|
|
37b5ba004c | ||
|
|
b774b47213 | ||
|
|
74dc00445e | ||
|
|
3b4563d5ea | ||
|
|
38669817b4 | ||
|
|
8f1d3ae04e | ||
|
|
74ed95871b | ||
|
|
cc5d67616c | ||
|
|
eed7f09e6f | ||
|
|
e3c1d23744 | ||
|
|
c4b1200b43 | ||
|
|
dff09f33a3 | ||
|
|
1f6b1dc2fe | ||
|
|
3f118dae93 | ||
|
|
91a30ff6fe | ||
|
|
5102669b06 | ||
|
|
2e2f09be00 | ||
|
|
c402103fe3 | ||
|
|
5e5df8abc8 | ||
|
|
d125eb7b16 | ||
|
|
a116858493 |
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 26
|
||||
buildToolsVersion '26.0.2'
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion '27.0.3'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 26
|
||||
targetSdkVersion 27
|
||||
|
||||
versionName "5.2.1"
|
||||
versionCode = 132
|
||||
versionName "5.6.6"
|
||||
versionCode = 146
|
||||
}
|
||||
|
||||
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,7 +50,42 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
release {
|
||||
// To whomever is releasing/using an APK in release mode with
|
||||
// Moonlight's official application ID, please stop. I see every
|
||||
// single one of your crashes in my Play Console and it makes
|
||||
// Moonlight's reliability look worse and makes it more difficult
|
||||
// to distinguish real crashes from your crashy VR app. Seriously,
|
||||
// 44 of the *same* native crash in 72 hours and a few each of
|
||||
// several other crashes.
|
||||
//
|
||||
// This is technically not your fault. I would have hoped Google
|
||||
// would validate the signature of the APK before attributing
|
||||
// the crash to it. I asked their Play Store support about this
|
||||
// and they said they don't and don't have plans to, so that sucks.
|
||||
//
|
||||
// In any case, it's bad form to release an APK using someone
|
||||
// else's application ID. There is no legitimate reason, that
|
||||
// anyone would need to comment out the following line, except me
|
||||
// when I release an official signed Moonlight build. If you feel
|
||||
// like doing so would solve something, I can tell you it will not.
|
||||
// You can't upgrade an app while retaining data without having the
|
||||
// same signature as the official version. Nor can you post it on
|
||||
// the Play Store, since that application ID is already taken.
|
||||
// Reputable APK hosting websites similarly validate the signature
|
||||
// is consistent with the Play Store and won't allow an APK that
|
||||
// isn't signed the same as the original.
|
||||
//
|
||||
// I wish any and all people using Moonlight as the basis of other
|
||||
// cool projects the best of luck with their efforts. All I ask
|
||||
// is to please change the applicationId before you publish.
|
||||
//
|
||||
// TL;DR: Leave the following line alone!
|
||||
applicationIdSuffix ".unofficial"
|
||||
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
@@ -60,9 +99,9 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.57'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.57'
|
||||
implementation files('libs/jcodec-0.1.9-patched.jar')
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
||||
implementation 'org.jcodec:jcodec:0.2.3'
|
||||
|
||||
implementation project(':moonlight-common')
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -103,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"
|
||||
|
||||
@@ -235,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);
|
||||
@@ -300,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
|
||||
|
||||
@@ -20,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;
|
||||
@@ -29,7 +30,9 @@ 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;
|
||||
@@ -37,6 +40,7 @@ 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;
|
||||
@@ -46,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;
|
||||
@@ -100,6 +105,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private ShortcutHelper shortcutHelper;
|
||||
|
||||
private MediaCodecDecoderRenderer decoderRenderer;
|
||||
private boolean reportedCrash;
|
||||
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
|
||||
@@ -125,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
|
||||
@@ -191,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);
|
||||
@@ -206,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();
|
||||
@@ -213,15 +222,51 @@ 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) {
|
||||
@@ -230,38 +275,91 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// 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));
|
||||
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;
|
||||
}
|
||||
|
||||
// Set to the optimal mode for streaming
|
||||
float displayRefreshRate = prepareDisplayForRendering();
|
||||
LimeLog.info("Display refresh rate: "+displayRefreshRate);
|
||||
|
||||
// HACK: Despite many efforts to ensure low latency consistent frame
|
||||
// delivery, the best non-lossy mechanism is to buffer 1 extra frame
|
||||
// in the output pipeline. Android does some buffering on its end
|
||||
// in SurfaceFlinger and it's difficult (impossible?) to inspect
|
||||
// the precise state of the buffer queue to the screen after we
|
||||
// release a frame for rendering.
|
||||
//
|
||||
// Since buffering a frame adds latency and we are primarily a
|
||||
// latency-optimized client, rather than one designed for picture-perfect
|
||||
// accuracy, we will synthetically induce a negative pressure on the display
|
||||
// output pipeline by driving the decoder input pipeline under the speed
|
||||
// that the display can refresh. This ensures a constant negative pressure
|
||||
// to keep latency down but does induce a periodic frame loss. However, this
|
||||
// periodic frame loss is *way* less than what we'd already get in Marshmallow's
|
||||
// display pipeline where frames are dropped outside of our control if they land
|
||||
// on the same V-sync.
|
||||
//
|
||||
// Hopefully, we can get rid of this once someone comes up with a better way
|
||||
// to track the state of the pipeline and time frames.
|
||||
int roundedRefreshRate = Math.round(displayRefreshRate);
|
||||
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) {
|
||||
if (roundedRefreshRate <= 49) {
|
||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
|
||||
}
|
||||
// HACK: Avoid crashing on some MTK devices
|
||||
else if (roundedRefreshRate == 50 && decoderRenderer.is49FpsBlacklisted()) {
|
||||
// Use the old rendering strategy on these broken devices
|
||||
decoderRenderer.enableLegacyFrameDropRendering();
|
||||
}
|
||||
else {
|
||||
prefConfig.fps = roundedRefreshRate - 1;
|
||||
LimeLog.info("Adjusting FPS target for screen to "+prefConfig.fps);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
|
||||
.setAudioConfiguration(prefConfig.enable51Surround ?
|
||||
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
|
||||
MoonBridge.AUDIO_CONFIGURATION_STEREO)
|
||||
@@ -274,9 +372,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||
|
||||
// Set to the optimal mode for streaming
|
||||
prepareDisplayForRendering();
|
||||
|
||||
// Initialize touch contexts
|
||||
for (int i = 0; i < touchContextMap.length; i++) {
|
||||
touchContextMap[i] = new TouchContext(conn, i,
|
||||
@@ -304,10 +399,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);
|
||||
@@ -322,9 +446,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareDisplayForRendering() {
|
||||
private float prepareDisplayForRendering() {
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||
float displayRefreshRate;
|
||||
|
||||
// On M, we can explicitly set the optimal display mode
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
@@ -362,6 +487,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||
displayRefreshRate = bestMode.getRefreshRate();
|
||||
}
|
||||
// On L, we can at least tell the OS that we want 60 Hz
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
@@ -374,6 +500,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
||||
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
||||
displayRefreshRate = bestRefreshRate;
|
||||
}
|
||||
else {
|
||||
// Otherwise, the active display refresh rate is just
|
||||
// whatever is currently in use.
|
||||
displayRefreshRate = display.getRefreshRate();
|
||||
}
|
||||
|
||||
// Apply the display mode change
|
||||
@@ -409,22 +541,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Set the surface to scale based on the aspect ratio of the stream
|
||||
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDataConnection()
|
||||
{
|
||||
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (mgr.isActiveNetworkMetered()) {
|
||||
displayTransientMessage(getResources().getString(R.string.conn_metered));
|
||||
}
|
||||
return displayRefreshRate;
|
||||
}
|
||||
|
||||
@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 |
|
||||
@@ -449,6 +581,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();
|
||||
@@ -497,7 +657,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) {
|
||||
@@ -509,9 +672,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
// Clear the tombstone count
|
||||
if (tombstonePrefs.getInt("CrashCount", 0) != 0) {
|
||||
tombstonePrefs.edit().putInt("CrashCount", 0).apply();
|
||||
// Clear the tombstone count if we terminated normally
|
||||
if (!reportedCrash && tombstonePrefs.getInt("CrashCount", 0) != 0) {
|
||||
tombstonePrefs.edit()
|
||||
.putInt("CrashCount", 0)
|
||||
.putInt("LastNotifiedCrashCount", 0)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,19 +699,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;
|
||||
}
|
||||
|
||||
@@ -557,7 +723,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))
|
||||
{
|
||||
@@ -638,7 +804,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;
|
||||
}
|
||||
|
||||
@@ -647,7 +813,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;
|
||||
@@ -678,7 +844,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
if (handleSpecialKeys(translated, false)) {
|
||||
if (handleSpecialKeys(keyCode, false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -948,7 +1114,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1107,7 +1283,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,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;
|
||||
@@ -31,8 +32,9 @@ import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
@@ -50,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 =
|
||||
@@ -85,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;
|
||||
@@ -151,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);
|
||||
@@ -164,32 +223,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
||||
|
||||
initializeViews();
|
||||
|
||||
SharedPreferences prefs = getSharedPreferences("DecoderTombstone", 0);
|
||||
int crashCount = prefs.getInt("CrashCount", 0);
|
||||
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
|
||||
|
||||
// Remember the last crash count we notified at, so we don't
|
||||
// display the crash dialog every time the app is started until
|
||||
// they stream again
|
||||
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
|
||||
if (crashCount % 3 == 0) {
|
||||
// At 3 consecutive crashes, we'll forcefully reset their settings
|
||||
PreferenceConfiguration.resetStreamingSettings(this);
|
||||
Dialog.displayDialog(this,
|
||||
getResources().getString(R.string.title_decoding_reset),
|
||||
getResources().getString(R.string.message_decoding_reset),
|
||||
false);
|
||||
}
|
||||
else {
|
||||
Dialog.displayDialog(this,
|
||||
getResources().getString(R.string.title_decoding_error),
|
||||
getResources().getString(R.string.message_decoding_error),
|
||||
false);
|
||||
}
|
||||
|
||||
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||
}
|
||||
}
|
||||
|
||||
private void startComputerUpdates() {
|
||||
@@ -245,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();
|
||||
}
|
||||
@@ -543,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:
|
||||
@@ -558,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;
|
||||
|
||||
@@ -176,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:
|
||||
|
||||
@@ -18,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;
|
||||
@@ -43,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;
|
||||
@@ -64,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;
|
||||
@@ -87,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:
|
||||
@@ -108,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:
|
||||
@@ -129,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;
|
||||
@@ -187,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:
|
||||
@@ -235,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,10 +11,13 @@ 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;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -72,10 +75,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)) {
|
||||
@@ -108,7 +123,17 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
// Do we have permission yet?
|
||||
if (!usbManager.hasPermission(device)) {
|
||||
// Let's ask for permission
|
||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
||||
try {
|
||||
// This function is not documented as throwing any exceptions (denying access
|
||||
// is indicated by calling the PendingIntent with a false result). However,
|
||||
// Samsung Knox has some policies which block this request, but rather than
|
||||
// just returning a false result or returning 0 enumerated devices,
|
||||
// they throw an undocumented SecurityException from this call, crashing
|
||||
// the whole app. :(
|
||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
||||
} catch (SecurityException e) {
|
||||
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,7 +169,7 @@ 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.
|
||||
@@ -169,7 +194,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) ||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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;
|
||||
@@ -12,13 +13,13 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
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 {
|
||||
@@ -32,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;
|
||||
@@ -51,6 +54,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
private CrashListener crashListener;
|
||||
private boolean reportedCrash;
|
||||
private int consecutiveCrashCount;
|
||||
private String glRenderer;
|
||||
private boolean foreground = true;
|
||||
private boolean legacyFrameDropRendering = false;
|
||||
|
||||
private boolean needsBaselineSpsHack;
|
||||
private SeqParameterSet savedSps;
|
||||
@@ -62,12 +68,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;
|
||||
@@ -81,9 +88,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;
|
||||
}
|
||||
|
||||
@@ -94,10 +101,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 {
|
||||
@@ -113,16 +121,19 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
this.renderTarget = renderTarget;
|
||||
}
|
||||
|
||||
public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver,
|
||||
CrashListener crashListener, int consecutiveCrashCount) {
|
||||
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 {
|
||||
@@ -137,7 +148,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());
|
||||
}
|
||||
@@ -152,10 +163,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");
|
||||
}
|
||||
@@ -176,6 +192,43 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
return avcDecoder != null;
|
||||
}
|
||||
|
||||
public boolean is49FpsBlacklisted() {
|
||||
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedFor49Fps(avcDecoder.getName());
|
||||
}
|
||||
|
||||
public void enableLegacyFrameDropRendering() {
|
||||
LimeLog.info("Legacy frame drop rendering enabled");
|
||||
legacyFrameDropRendering = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -190,7 +243,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();
|
||||
|
||||
@@ -219,7 +272,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();
|
||||
|
||||
@@ -367,7 +420,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
// Render the last buffer
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !legacyFrameDropRendering) {
|
||||
// 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);
|
||||
@@ -400,32 +462,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();
|
||||
|
||||
@@ -438,14 +544,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;
|
||||
@@ -473,34 +589,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
|
||||
@@ -548,22 +643,28 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
// High Profile which allows the decoder to assume there will be no B-frames and
|
||||
// reduce delay and buffering accordingly. Some devices (Marvell, Exynos 4) don't
|
||||
// like it so we only set them on devices that are confirmed to benefit from it.
|
||||
if (sps.profile_idc == 100 && constrainedHighProfile) {
|
||||
if (sps.profileIdc == 100 && constrainedHighProfile) {
|
||||
LimeLog.info("Setting constraint set flags for constrained high profile");
|
||||
sps.constraint_set_4_flag = true;
|
||||
sps.constraint_set_5_flag = true;
|
||||
sps.constraintSet4Flag = true;
|
||||
sps.constraintSet5Flag = true;
|
||||
}
|
||||
else {
|
||||
// Force the constraints unset otherwise (some may be set by default)
|
||||
sps.constraint_set_4_flag = false;
|
||||
sps.constraint_set_5_flag = false;
|
||||
sps.constraintSet4Flag = false;
|
||||
sps.constraintSet5Flag = false;
|
||||
}
|
||||
}
|
||||
|
||||
@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.
|
||||
@@ -593,14 +694,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);
|
||||
@@ -617,12 +716,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
if (initialWidth == 1280 && initialHeight == 720) {
|
||||
// Max 5 buffered frames at 1280x720x60
|
||||
LimeLog.info("Patching level_idc to 32");
|
||||
sps.level_idc = 32;
|
||||
sps.levelIdc = 32;
|
||||
}
|
||||
else if (initialWidth == 1920 && initialHeight == 1080) {
|
||||
// Max 4 buffered frames at 1920x1080x64
|
||||
LimeLog.info("Patching level_idc to 42");
|
||||
sps.level_idc = 42;
|
||||
sps.levelIdc = 42;
|
||||
}
|
||||
else {
|
||||
// Leave the profile alone (currently 5.0)
|
||||
@@ -640,14 +739,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
// where we've enabled reference frame invalidation.
|
||||
if (!refFrameInvalidationActive) {
|
||||
LimeLog.info("Patching num_ref_frames in SPS");
|
||||
sps.num_ref_frames = 1;
|
||||
sps.numRefFrames = 1;
|
||||
}
|
||||
|
||||
// GFE 2.5.11 changed the SPS to add additional extensions
|
||||
// Some devices don't like these so we remove them here.
|
||||
sps.vuiParams.video_signal_type_present_flag = false;
|
||||
sps.vuiParams.colour_description_present_flag = false;
|
||||
sps.vuiParams.chroma_loc_info_present_flag = false;
|
||||
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||
|
||||
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
|
||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||
@@ -657,22 +756,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
if (sps.vuiParams.bitstreamRestriction == null) {
|
||||
LimeLog.info("Adding bitstream restrictions");
|
||||
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
||||
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
|
||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
|
||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
|
||||
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
||||
sps.vuiParams.bitstreamRestriction.motionVectorsOverPicBoundariesFlag = true;
|
||||
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthHorizontal = 16;
|
||||
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthVertical = 16;
|
||||
sps.vuiParams.bitstreamRestriction.numReorderFrames = 0;
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Patching bitstream restrictions");
|
||||
}
|
||||
|
||||
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
|
||||
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
|
||||
// Some devices throw errors if maxDecFrameBuffering < numRefFrames
|
||||
sps.vuiParams.bitstreamRestriction.maxDecFrameBuffering = sps.numRefFrames;
|
||||
|
||||
// These values are the defaults for the fields, but they are more aggressive
|
||||
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
|
||||
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
|
||||
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
|
||||
sps.vuiParams.bitstreamRestriction.maxBytesPerPicDenom = 2;
|
||||
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
|
||||
|
||||
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
|
||||
// conservative values by GFE 2.5.11. We'll let those values stand.
|
||||
@@ -686,109 +785,81 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
// If we need to hack this SPS to say we're baseline, do so now
|
||||
if (needsBaselineSpsHack) {
|
||||
LimeLog.info("Hacking SPS to baseline");
|
||||
sps.profile_idc = 66;
|
||||
sps.profileIdc = 66;
|
||||
savedSps = sps;
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -802,10 +873,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(),
|
||||
@@ -813,12 +908,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;
|
||||
@@ -839,7 +940,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||
|
||||
// Switch the H264 profile back to high
|
||||
savedSps.profile_idc = 100;
|
||||
savedSps.profileIdc = 100;
|
||||
|
||||
// Patch the SPS constraint flags
|
||||
doProfileSpecificSpsPatching(savedSps);
|
||||
@@ -883,55 +984,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";
|
||||
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 += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\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: ";
|
||||
@@ -945,6 +1073,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,13 +28,18 @@ 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;
|
||||
private static final List<String> whitelistedHevcDecoders;
|
||||
private static final List<String> refFrameInvalidationAvcPrefixes;
|
||||
private static final List<String> refFrameInvalidationHevcPrefixes;
|
||||
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
||||
|
||||
private static boolean isLowEndSnapdragon = false;
|
||||
private static boolean initialized = false;
|
||||
|
||||
static {
|
||||
directSubmitPrefixes = new LinkedList<>();
|
||||
@@ -92,11 +98,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");
|
||||
@@ -113,8 +121,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
|
||||
@@ -132,20 +146,90 @@ 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.
|
||||
}
|
||||
|
||||
static {
|
||||
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
|
||||
|
||||
// We see a bunch of crashes on MediaTek Android TVs running
|
||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
||||
// these devices and hope they fix it in Oreo.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getAdrenoVersionString(String glRenderer) {
|
||||
glRenderer = glRenderer.toLowerCase().trim();
|
||||
|
||||
if (!glRenderer.contains("adreno")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
|
||||
|
||||
Matcher matcher = modelNumberPattern.matcher(glRenderer);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String modelNumber = matcher.group(2);
|
||||
LimeLog.info("Found Adreno GPU: "+modelNumber);
|
||||
return modelNumber;
|
||||
}
|
||||
|
||||
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
|
||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
||||
if (modelNumber == null) {
|
||||
// Not an Adreno GPU
|
||||
return false;
|
||||
}
|
||||
|
||||
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
|
||||
return modelNumber.charAt(1) == '0';
|
||||
}
|
||||
|
||||
// This is a workaround for some broken devices that report
|
||||
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
|
||||
// An example of such a device is the Huawei Honor 5x with the
|
||||
// Snapdragon 616 SoC (Adreno 405).
|
||||
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
|
||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
||||
if (modelNumber == null) {
|
||||
// Not an Adreno GPU
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snapdragon 4xx and higher support GLES 3.1
|
||||
return modelNumber.charAt(0) >= '4';
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -160,22 +244,30 @@ public class MediaCodecHelper {
|
||||
// 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
|
||||
// The "good" GPUs support GLES 3.1, but we can't just check that directly
|
||||
// (see comment on isGLES31SnapdragonRenderer).
|
||||
//
|
||||
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
||||
// 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");
|
||||
// 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,20 +283,15 @@ public class MediaCodecHelper {
|
||||
public static long getMonotonicMillis() {
|
||||
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))
|
||||
@@ -216,7 +303,7 @@ public class MediaCodecHelper {
|
||||
} catch (Exception e) {
|
||||
// Tolerate buggy codecs
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -237,7 +324,22 @@ public class MediaCodecHelper {
|
||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName) {
|
||||
public static boolean decoderBlacklistedFor49Fps(String decoderName) {
|
||||
return isDecoderInList(blacklisted49FpsDecoderPrefixes, 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);
|
||||
}
|
||||
|
||||
@@ -245,7 +347,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
|
||||
@@ -277,6 +379,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);
|
||||
}
|
||||
|
||||
@@ -324,6 +435,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()) {
|
||||
|
||||
@@ -3,8 +3,10 @@ package com.limelight.computers;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -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) {
|
||||
@@ -367,6 +369,10 @@ public class ComputerManagerService extends Service {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (!manuallyAdded) {
|
||||
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -386,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;
|
||||
@@ -500,14 +507,26 @@ 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.localAddress.equals(details.remoteAddress)) {
|
||||
reachability = ComputerDetails.Reachability.REMOTE;
|
||||
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
|
||||
@@ -553,7 +572,14 @@ public class ComputerManagerService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (polledDetails.remoteAddress.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.localAddress.equals(reachableAddr)) {
|
||||
@@ -815,6 +841,7 @@ public class ComputerManagerService extends Service {
|
||||
} while (waitPollingDelay());
|
||||
}
|
||||
};
|
||||
thread.setName("App list polling thread for " + computer.localAddress);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -22,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;
|
||||
@@ -45,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;
|
||||
@@ -59,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,12 +143,13 @@ public class PreferenceConfiguration {
|
||||
}
|
||||
|
||||
public static void resetStreamingSettings(Context context) {
|
||||
// We consider resolution, FPS, bitrate, and video format as "streaming settings" here
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -200,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,16 +2,23 @@ package com.limelight.preferences;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
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;
|
||||
|
||||
public class StreamSettings extends Activity {
|
||||
@@ -50,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);
|
||||
@@ -65,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,22 +2,42 @@ package com.limelight.utils;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class ServerHelper {
|
||||
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);
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<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>
|
||||
<string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">Establishing Connection</string>
|
||||
@@ -110,6 +111,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>
|
||||
@@ -126,6 +129,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>
|
||||
@@ -142,7 +147,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.0-beta7'
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Submodule moonlight-common updated: 83423959a1...21e1bc8c35
Reference in New Issue
Block a user