mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-21 15:56:57 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37ddccde0c | ||
|
|
ffc59c6bd6 | ||
|
|
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 |
No files matched your search
+50
-6
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 27
|
||||||
buildToolsVersion '27.0.1'
|
buildToolsVersion '27.0.3'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
|
|
||||||
versionName "5.6"
|
versionName "5.6.7"
|
||||||
versionCode = 138
|
versionCode = 147
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "root"
|
flavorDimensions "root"
|
||||||
@@ -23,11 +23,23 @@ android {
|
|||||||
// version to devices running O on the Play Store.
|
// version to devices running O on the Play Store.
|
||||||
maxSdkVersion 25
|
maxSdkVersion 25
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
arguments "PRODUCT_FLAVOR=root"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applicationId "com.limelight.root"
|
applicationId "com.limelight.root"
|
||||||
dimension "root"
|
dimension "root"
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
arguments "PRODUCT_FLAVOR=nonRoot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applicationId "com.limelight"
|
applicationId "com.limelight"
|
||||||
dimension "root"
|
dimension "root"
|
||||||
}
|
}
|
||||||
@@ -42,6 +54,38 @@ android {
|
|||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
}
|
}
|
||||||
release {
|
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
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||||
}
|
}
|
||||||
@@ -55,9 +99,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.57'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.57'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
||||||
implementation files('libs/jcodec-0.1.9-patched.jar')
|
implementation 'org.jcodec:jcodec:0.2.3'
|
||||||
|
|
||||||
implementation project(':moonlight-common')
|
implementation project(':moonlight-common')
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -294,7 +294,58 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
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();
|
Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setRefreshRate(prefConfig.fps)
|
||||||
@@ -302,11 +353,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
.setBitrate(prefConfig.bitrate * 1000)
|
.setBitrate(prefConfig.bitrate * 1000)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
.setMaxPacketSize(remote ? 1024 : 1292)
|
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292)
|
||||||
.setRemote(remote)
|
.setRemote(remote)
|
||||||
.setHevcBitratePercentageMultiplier(75)
|
.setHevcBitratePercentageMultiplier(75)
|
||||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||||
.setEnableHdr(willStreamHdr)
|
.setEnableHdr(willStreamHdr)
|
||||||
|
.setAttachedGamepadMask(gamepadMask)
|
||||||
|
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
|
||||||
.setAudioConfiguration(prefConfig.enable51Surround ?
|
.setAudioConfiguration(prefConfig.enable51Surround ?
|
||||||
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
|
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
|
||||||
MoonBridge.AUDIO_CONFIGURATION_STEREO)
|
MoonBridge.AUDIO_CONFIGURATION_STEREO)
|
||||||
@@ -319,9 +372,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
inputManager.registerInputDeviceListener(controllerHandler, null);
|
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||||
|
|
||||||
// Set to the optimal mode for streaming
|
|
||||||
prepareDisplayForRendering();
|
|
||||||
|
|
||||||
// Initialize touch contexts
|
// Initialize touch contexts
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
touchContextMap[i] = new TouchContext(conn, i,
|
touchContextMap[i] = new TouchContext(conn, i,
|
||||||
@@ -396,9 +446,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareDisplayForRendering() {
|
private float prepareDisplayForRendering() {
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||||
|
float displayRefreshRate;
|
||||||
|
|
||||||
// On M, we can explicitly set the optimal display mode
|
// On M, we can explicitly set the optimal display mode
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@@ -436,6 +487,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||||
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||||
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||||
|
displayRefreshRate = bestMode.getRefreshRate();
|
||||||
}
|
}
|
||||||
// On L, we can at least tell the OS that we want 60 Hz
|
// On L, we can at least tell the OS that we want 60 Hz
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
@@ -448,6 +500,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
||||||
windowLayoutParams.preferredRefreshRate = 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
|
// Apply the display mode change
|
||||||
@@ -483,6 +541,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Set the surface to scale based on the aspect ratio of the stream
|
// Set the surface to scale based on the aspect ratio of the stream
|
||||||
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return displayRefreshRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
|
|||||||
@@ -176,14 +176,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
public void start() {}
|
public void start() {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {}
|
||||||
// Immediately drop all pending data
|
|
||||||
track.pause();
|
|
||||||
track.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
|
// Immediately drop all pending data
|
||||||
|
track.pause();
|
||||||
|
track.flush();
|
||||||
|
|
||||||
track.release();
|
track.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbManager;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
@@ -12,6 +14,7 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.input.driver.UsbDriverListener;
|
import com.limelight.binding.input.driver.UsbDriverListener;
|
||||||
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
@@ -48,7 +51,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private boolean hasGameController;
|
private boolean hasGameController;
|
||||||
|
|
||||||
private final boolean multiControllerEnabled;
|
private final boolean multiControllerEnabled;
|
||||||
private short currentControllers;
|
private short currentControllers, initialControllers;
|
||||||
|
|
||||||
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
||||||
this.activityContext = activityContext;
|
this.activityContext = activityContext;
|
||||||
@@ -102,6 +105,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// consume these. Instead, let's ignore them since that's probably the
|
// consume these. Instead, let's ignore them since that's probably the
|
||||||
// most likely case.
|
// most likely case.
|
||||||
defaultContext.ignoreBack = true;
|
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) {
|
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||||
@@ -139,8 +148,47 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
onInputDeviceAdded(deviceId);
|
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) {
|
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) {
|
if (context.assignedControllerNumber) {
|
||||||
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
|
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
|
||||||
(short) 0,
|
(short) 0,
|
||||||
@@ -148,12 +196,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
(short) 0, (short) 0,
|
(short) 0, (short) 0,
|
||||||
(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
|
// Called before sending input but after we've determined that this
|
||||||
@@ -181,6 +223,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if ((currentControllers & (1 << i)) == 0) {
|
if ((currentControllers & (1 << i)) == 0) {
|
||||||
// Found an unused controller value
|
// Found an unused controller value
|
||||||
currentControllers |= (1 << i);
|
currentControllers |= (1 << i);
|
||||||
|
|
||||||
|
// Take this value out of the initial gamepad set
|
||||||
|
initialControllers &= ~(1 << i);
|
||||||
|
|
||||||
context.controllerNumber = i;
|
context.controllerNumber = i;
|
||||||
context.reservedControllerNumber = true;
|
context.reservedControllerNumber = true;
|
||||||
break;
|
break;
|
||||||
@@ -201,6 +247,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if ((currentControllers & (1 << i)) == 0) {
|
if ((currentControllers & (1 << i)) == 0) {
|
||||||
// Found an unused controller value
|
// Found an unused controller value
|
||||||
currentControllers |= (1 << i);
|
currentControllers |= (1 << i);
|
||||||
|
|
||||||
|
// Take this value out of the initial gamepad set
|
||||||
|
initialControllers &= ~(1 << i);
|
||||||
|
|
||||||
context.controllerNumber = i;
|
context.controllerNumber = i;
|
||||||
context.reservedControllerNumber = true;
|
context.reservedControllerNumber = true;
|
||||||
break;
|
break;
|
||||||
@@ -473,7 +523,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
private short getActiveControllerMask() {
|
private short getActiveControllerMask() {
|
||||||
if (multiControllerEnabled) {
|
if (multiControllerEnabled) {
|
||||||
return currentControllers;
|
return (short)(currentControllers | initialControllers);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Only Player 1 is active with multi-controller disabled
|
// Only Player 1 is active with multi-controller disabled
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package com.limelight.binding.input.capture;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
import com.limelight.R;
|
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;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
|
||||||
public class InputCaptureManager {
|
public class InputCaptureManager {
|
||||||
@@ -13,13 +14,15 @@ public class InputCaptureManager {
|
|||||||
LimeLog.info("Using Android O+ native mouse capture");
|
LimeLog.info("Using Android O+ native mouse capture");
|
||||||
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
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");
|
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||||
return new ShieldCaptureProvider(activity);
|
return new ShieldCaptureProvider(activity);
|
||||||
}
|
}
|
||||||
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
|
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
|
||||||
LimeLog.info("Using Evdev mouse capture");
|
LimeLog.info("Using Evdev mouse capture");
|
||||||
return new EvdevCaptureProvider(activity, rootListener);
|
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
|
||||||
}
|
}
|
||||||
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
||||||
// Android N's native capture can't capture over system UI elements
|
// Android N's native capture can't capture over system UI elements
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import android.os.Build;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@@ -121,7 +123,17 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
// Do we have permission yet?
|
// Do we have permission yet?
|
||||||
if (!usbManager.hasPermission(device)) {
|
if (!usbManager.hasPermission(device)) {
|
||||||
// Let's ask for permission
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,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
|
// 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
|
// 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.
|
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||||
@@ -182,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
|
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||||
// if we know the kernel isn't already driving this device.
|
// if we know the kernel isn't already driving this device.
|
||||||
return XboxOneController.canClaimDevice(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private int consecutiveCrashCount;
|
private int consecutiveCrashCount;
|
||||||
private String glRenderer;
|
private String glRenderer;
|
||||||
private boolean foreground = true;
|
private boolean foreground = true;
|
||||||
|
private boolean legacyFrameDropRendering = false;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
private SeqParameterSet savedSps;
|
private SeqParameterSet savedSps;
|
||||||
@@ -100,10 +101,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// for even required levels of HEVC.
|
// for even required levels of HEVC.
|
||||||
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
||||||
if (decoderInfo != null) {
|
if (decoderInfo != null) {
|
||||||
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork, requestedHdr)) {
|
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork)) {
|
||||||
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
|
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
|
||||||
|
|
||||||
if (prefs.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");
|
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -165,6 +167,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
|
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
|
||||||
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
|
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
|
||||||
|
|
||||||
|
if (consecutiveCrashCount % 2 == 1) {
|
||||||
|
refFrameInvalidationAvc = refFrameInvalidationHevc = false;
|
||||||
|
LimeLog.warning("Disabling RFI due to previous crash");
|
||||||
|
}
|
||||||
|
|
||||||
if (directSubmit) {
|
if (directSubmit) {
|
||||||
LimeLog.info("Decoder "+avcDecoder.getName()+" will use direct submit");
|
LimeLog.info("Decoder "+avcDecoder.getName()+" will use direct submit");
|
||||||
}
|
}
|
||||||
@@ -185,6 +192,15 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return avcDecoder != null;
|
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() {
|
public boolean isHevcMain10Hdr10Supported() {
|
||||||
if (hevcDecoder == null) {
|
if (hevcDecoder == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -404,7 +420,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the last buffer
|
// Render the last buffer
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && prefs.disableFrameDrop) {
|
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
|
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
||||||
// is disabled
|
// is disabled
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||||
@@ -573,25 +589,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// May be called already, but we'll call it now to be safe
|
// May be called already, but we'll call it now to be safe
|
||||||
prepareForStop();
|
prepareForStop();
|
||||||
|
|
||||||
try {
|
|
||||||
// Invalidate pending decode buffers
|
|
||||||
videoDecoder.flush();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the renderer thread to shut down
|
// Wait for the renderer thread to shut down
|
||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException ignored) { }
|
} catch (InterruptedException ignored) { }
|
||||||
|
|
||||||
try {
|
|
||||||
// Stop the video decoder
|
|
||||||
videoDecoder.stop();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Halt the spinner threads
|
// Halt the spinner threads
|
||||||
stopSpinnerThreads();
|
stopSpinnerThreads();
|
||||||
}
|
}
|
||||||
@@ -641,15 +643,15 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// High Profile which allows the decoder to assume there will be no B-frames and
|
// 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
|
// 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.
|
// 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");
|
LimeLog.info("Setting constraint set flags for constrained high profile");
|
||||||
sps.constraint_set_4_flag = true;
|
sps.constraintSet4Flag = true;
|
||||||
sps.constraint_set_5_flag = true;
|
sps.constraintSet5Flag = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Force the constraints unset otherwise (some may be set by default)
|
// Force the constraints unset otherwise (some may be set by default)
|
||||||
sps.constraint_set_4_flag = false;
|
sps.constraintSet4Flag = false;
|
||||||
sps.constraint_set_5_flag = false;
|
sps.constraintSet5Flag = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,6 +659,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||||
int frameNumber, long receiveTimeMs) {
|
int frameNumber, long receiveTimeMs) {
|
||||||
|
if (stopping) {
|
||||||
|
// Don't bother if we're stopping
|
||||||
|
return MoonBridge.DR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
totalFramesReceived++;
|
totalFramesReceived++;
|
||||||
|
|
||||||
// We can receive the same "frame" multiple times if it's an IDR frame.
|
// We can receive the same "frame" multiple times if it's an IDR frame.
|
||||||
@@ -709,12 +716,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
if (initialWidth == 1280 && initialHeight == 720) {
|
if (initialWidth == 1280 && initialHeight == 720) {
|
||||||
// Max 5 buffered frames at 1280x720x60
|
// Max 5 buffered frames at 1280x720x60
|
||||||
LimeLog.info("Patching level_idc to 32");
|
LimeLog.info("Patching level_idc to 32");
|
||||||
sps.level_idc = 32;
|
sps.levelIdc = 32;
|
||||||
}
|
}
|
||||||
else if (initialWidth == 1920 && initialHeight == 1080) {
|
else if (initialWidth == 1920 && initialHeight == 1080) {
|
||||||
// Max 4 buffered frames at 1920x1080x64
|
// Max 4 buffered frames at 1920x1080x64
|
||||||
LimeLog.info("Patching level_idc to 42");
|
LimeLog.info("Patching level_idc to 42");
|
||||||
sps.level_idc = 42;
|
sps.levelIdc = 42;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Leave the profile alone (currently 5.0)
|
// Leave the profile alone (currently 5.0)
|
||||||
@@ -732,14 +739,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// where we've enabled reference frame invalidation.
|
// where we've enabled reference frame invalidation.
|
||||||
if (!refFrameInvalidationActive) {
|
if (!refFrameInvalidationActive) {
|
||||||
LimeLog.info("Patching num_ref_frames in SPS");
|
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
|
// GFE 2.5.11 changed the SPS to add additional extensions
|
||||||
// Some devices don't like these so we remove them here.
|
// Some devices don't like these so we remove them here.
|
||||||
sps.vuiParams.video_signal_type_present_flag = false;
|
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||||
sps.vuiParams.colour_description_present_flag = false;
|
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||||
sps.vuiParams.chroma_loc_info_present_flag = false;
|
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||||
|
|
||||||
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
|
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
|
||||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||||
@@ -749,22 +756,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
if (sps.vuiParams.bitstreamRestriction == null) {
|
if (sps.vuiParams.bitstreamRestriction == null) {
|
||||||
LimeLog.info("Adding bitstream restrictions");
|
LimeLog.info("Adding bitstream restrictions");
|
||||||
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
||||||
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
|
sps.vuiParams.bitstreamRestriction.motionVectorsOverPicBoundariesFlag = true;
|
||||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
|
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthHorizontal = 16;
|
||||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
|
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthVertical = 16;
|
||||||
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
sps.vuiParams.bitstreamRestriction.numReorderFrames = 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LimeLog.info("Patching bitstream restrictions");
|
LimeLog.info("Patching bitstream restrictions");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
|
// Some devices throw errors if maxDecFrameBuffering < numRefFrames
|
||||||
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
|
sps.vuiParams.bitstreamRestriction.maxDecFrameBuffering = sps.numRefFrames;
|
||||||
|
|
||||||
// These values are the defaults for the fields, but they are more aggressive
|
// 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.
|
// 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.maxBytesPerPicDenom = 2;
|
||||||
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
|
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
|
||||||
|
|
||||||
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
|
// 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.
|
// conservative values by GFE 2.5.11. We'll let those values stand.
|
||||||
@@ -778,7 +785,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// If we need to hack this SPS to say we're baseline, do so now
|
// If we need to hack this SPS to say we're baseline, do so now
|
||||||
if (needsBaselineSpsHack) {
|
if (needsBaselineSpsHack) {
|
||||||
LimeLog.info("Hacking SPS to baseline");
|
LimeLog.info("Hacking SPS to baseline");
|
||||||
sps.profile_idc = 66;
|
sps.profileIdc = 66;
|
||||||
savedSps = sps;
|
savedSps = sps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,6 +889,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Copy data from our buffer list into the input buffer
|
||||||
buf.put(decodeUnitData, 0, decodeUnitLength);
|
buf.put(decodeUnitData, 0, decodeUnitLength);
|
||||||
|
|
||||||
@@ -923,7 +940,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||||
|
|
||||||
// Switch the H264 profile back to high
|
// Switch the H264 profile back to high
|
||||||
savedSps.profile_idc = 100;
|
savedSps.profileIdc = 100;
|
||||||
|
|
||||||
// Patch the SPS constraint flags
|
// Patch the SPS constraint flags
|
||||||
doProfileSpecificSpsPatching(savedSps);
|
doProfileSpecificSpsPatching(savedSps);
|
||||||
@@ -1033,6 +1050,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
||||||
str += "Foreground: "+renderer.foreground+"\n";
|
str += "Foreground: "+renderer.foreground+"\n";
|
||||||
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
||||||
|
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
|
||||||
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||||
str += "FPS target: "+renderer.refreshRate+"\n";
|
str += "FPS target: "+renderer.refreshRate+"\n";
|
||||||
str += "Bitrate: "+renderer.prefs.bitrate+" Mbps \n";
|
str += "Bitrate: "+renderer.prefs.bitrate+" Mbps \n";
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class MediaCodecHelper {
|
|||||||
private static final List<String> whitelistedHevcDecoders;
|
private static final List<String> whitelistedHevcDecoders;
|
||||||
private static final List<String> refFrameInvalidationAvcPrefixes;
|
private static final List<String> refFrameInvalidationAvcPrefixes;
|
||||||
private static final List<String> refFrameInvalidationHevcPrefixes;
|
private static final List<String> refFrameInvalidationHevcPrefixes;
|
||||||
|
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragon = false;
|
private static boolean isLowEndSnapdragon = false;
|
||||||
private static boolean initialized = false;
|
private static boolean initialized = false;
|
||||||
@@ -97,10 +98,13 @@ public class MediaCodecHelper {
|
|||||||
baselineProfileHackPrefixes = new LinkedList<>();
|
baselineProfileHackPrefixes = new LinkedList<>();
|
||||||
baselineProfileHackPrefixes.add("omx.intel");
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
|
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
|
||||||
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
|
// 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.
|
// if adaptive playback was enabled so let's avoid it to be safe.
|
||||||
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
|
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
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 = new LinkedList<>();
|
||||||
constrainedHighProfilePrefixes.add("omx.intel");
|
constrainedHighProfilePrefixes.add("omx.intel");
|
||||||
@@ -150,27 +154,62 @@ public class MediaCodecHelper {
|
|||||||
// Qualcomm is currently the only decoders in this group.
|
// Qualcomm is currently the only decoders in this group.
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
|
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();
|
glRenderer = glRenderer.toLowerCase().trim();
|
||||||
|
|
||||||
if (!glRenderer.contains("adreno")) {
|
if (!glRenderer.contains("adreno")) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
|
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
|
||||||
|
|
||||||
Matcher matcher = modelNumberPattern.matcher(glRenderer);
|
Matcher matcher = modelNumberPattern.matcher(glRenderer);
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String modelNumber = matcher.group(2);
|
String modelNumber = matcher.group(2);
|
||||||
LimeLog.info("Found Adreno GPU: "+modelNumber);
|
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.
|
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
|
||||||
return modelNumber.charAt(1) == '0';
|
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) {
|
public static void initialize(Context context, String glRenderer) {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return;
|
return;
|
||||||
@@ -205,9 +244,10 @@ public class MediaCodecHelper {
|
|||||||
// 3xx - bad
|
// 3xx - bad
|
||||||
// 4xx - good
|
// 4xx - good
|
||||||
//
|
//
|
||||||
// Unfortunately, it's not that easy to get that information here, so I'll use an
|
// The "good" GPUs support GLES 3.1, but we can't just check that directly
|
||||||
// approximation by checking the GLES level (<= 3.0 is bad).
|
// (see comment on isGLES31SnapdragonRenderer).
|
||||||
if (configInfo.reqGlEsVersion > 0x30000) {
|
//
|
||||||
|
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
||||||
// We prefer reference frame invalidation support (which is only doable on AVC on
|
// 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
|
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
||||||
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
|
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
|
||||||
@@ -243,7 +283,7 @@ public class MediaCodecHelper {
|
|||||||
public static long getMonotonicMillis() {
|
public static long getMonotonicMillis() {
|
||||||
return System.nanoTime() / 1000000L;
|
return System.nanoTime() / 1000000L;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
|
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
|
||||||
// Possibly enable adaptive playback on KitKat and above
|
// Possibly enable adaptive playback on KitKat and above
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
@@ -284,11 +324,22 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean decoderBlacklistedFor49Fps(String decoderName) {
|
||||||
|
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
||||||
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
||||||
if (videoHeight > 720 && isLowEndSnapdragon) {
|
if (videoHeight > 720 && isLowEndSnapdragon) {
|
||||||
return false;
|
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);
|
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +347,7 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData, boolean willStreamHdr) {
|
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
|
||||||
// TODO: Shield Tablet K1/LTE?
|
// TODO: Shield Tablet K1/LTE?
|
||||||
//
|
//
|
||||||
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
||||||
@@ -332,7 +383,7 @@ public class MediaCodecHelper {
|
|||||||
// typically because it can't support reference frame invalidation.
|
// typically because it can't support reference frame invalidation.
|
||||||
// However, we will use it for HDR and for streaming over mobile networks
|
// However, we will use it for HDR and for streaming over mobile networks
|
||||||
// since it works fine otherwise.
|
// since it works fine otherwise.
|
||||||
if ((meteredData || willStreamHdr) && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
||||||
LimeLog.info("Selected deprioritized decoder");
|
LimeLog.info("Selected deprioritized decoder");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.limelight.computers;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
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_POLLING_PERIOD_MS = 30000;
|
||||||
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
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 OFFLINE_POLL_TRIES = 5;
|
||||||
private static final int INITIAL_POLL_TRIES = 2;
|
private static final int INITIAL_POLL_TRIES = 2;
|
||||||
private static final int EMPTY_LIST_THRESHOLD = 3;
|
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||||
@@ -132,7 +134,7 @@ public class ComputerManagerService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
int offlineCount = 0;
|
int offlineCount = 0;
|
||||||
while (!isInterrupted() && pollingActive) {
|
while (!isInterrupted() && pollingActive && tuple.thread == this) {
|
||||||
try {
|
try {
|
||||||
// Only allow one request to the machine at a time
|
// Only allow one request to the machine at a time
|
||||||
synchronized (tuple.networkLock) {
|
synchronized (tuple.networkLock) {
|
||||||
@@ -367,6 +369,10 @@ public class ComputerManagerService extends Service {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (!manuallyAdded) {
|
||||||
|
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,6 +392,7 @@ public class ComputerManagerService extends Service {
|
|||||||
if (tuple.thread != null) {
|
if (tuple.thread != null) {
|
||||||
// Interrupt the thread on this entry
|
// Interrupt the thread on this entry
|
||||||
tuple.thread.interrupt();
|
tuple.thread.interrupt();
|
||||||
|
tuple.thread = null;
|
||||||
}
|
}
|
||||||
pollingTuples.remove(tuple);
|
pollingTuples.remove(tuple);
|
||||||
break;
|
break;
|
||||||
@@ -500,14 +507,26 @@ public class ComputerManagerService extends Service {
|
|||||||
return ComputerDetails.Reachability.OFFLINE;
|
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 {
|
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
||||||
ComputerDetails polledDetails;
|
ComputerDetails polledDetails;
|
||||||
ComputerDetails.Reachability reachability;
|
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)) {
|
if (details.localAddress.equals(details.remoteAddress)) {
|
||||||
reachability = ComputerDetails.Reachability.REMOTE;
|
reachability = isAddressLikelyLocal(details.localAddress) ?
|
||||||
|
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||||
@@ -553,7 +572,14 @@ public class ComputerManagerService extends Service {
|
|||||||
return null;
|
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;
|
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else if (polledDetails.localAddress.equals(reachableAddr)) {
|
else if (polledDetails.localAddress.equals(reachableAddr)) {
|
||||||
@@ -815,6 +841,7 @@ public class ComputerManagerService extends Service {
|
|||||||
} while (waitPollingDelay());
|
} while (waitPollingDelay());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
thread.setName("App list polling thread for " + computer.localAddress);
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import java.io.OutputStream;
|
|||||||
|
|
||||||
public class DiskAssetLoader {
|
public class DiskAssetLoader {
|
||||||
// 5 MB
|
// 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;
|
private final File cacheDir;
|
||||||
|
|
||||||
@@ -25,33 +29,65 @@ public class DiskAssetLoader {
|
|||||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
||||||
InputStream in = null;
|
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
||||||
Bitmap bmp = null;
|
// Raw height and width of image
|
||||||
try {
|
final int height = options.outHeight;
|
||||||
// Make sure the cached asset doesn't exceed the maximum size
|
final int width = options.outWidth;
|
||||||
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
int inSampleSize = 1;
|
||||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
|
||||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
if (height > reqHeight || width > reqWidth) {
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = sampleSize;
|
final int halfHeight = height / 2;
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
final int halfWidth = width / 2;
|
||||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
|
||||||
} catch (IOException ignored) {
|
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
|
||||||
} finally {
|
// height and width larger than the requested height and width.
|
||||||
if (in != null) {
|
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||||
try {
|
inSampleSize *= 2;
|
||||||
in.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
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;
|
return bmp;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.io.OutputStream;
|
|||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
||||||
public class CacheHelper {
|
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;
|
File f = root;
|
||||||
for (int i = 0; i < path.length; i++) {
|
for (int i = 0; i < path.length; i++) {
|
||||||
String component = path[i];
|
String component = path[i];
|
||||||
|
|||||||
@@ -5,28 +5,32 @@ include $(call all-subdir-makefiles)
|
|||||||
|
|
||||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
# Only build evdev_reader for the rooted APK flavor
|
||||||
LOCAL_MODULE := evdev_reader
|
ifeq (root,$(PRODUCT_FLAVOR))
|
||||||
LOCAL_SRC_FILES := evdev_reader.c
|
include $(CLEAR_VARS)
|
||||||
LOCAL_LDLIBS := -llog
|
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
|
# 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
|
# creates a binary with the libXXX.so form so the APK will install and drop
|
||||||
# the binary correctly.
|
# the binary correctly.
|
||||||
|
|
||||||
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
||||||
LOCAL_MAKEFILE := $(local-makefile)
|
LOCAL_MAKEFILE := $(local-makefile)
|
||||||
|
|
||||||
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
||||||
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
||||||
$(call check-LOCAL_MODULE_FILENAME)
|
$(call check-LOCAL_MODULE_FILENAME)
|
||||||
|
|
||||||
# we are building target objects
|
# we are building target objects
|
||||||
my := TARGET_
|
my := TARGET_
|
||||||
|
|
||||||
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
||||||
$(call handle-module-built)
|
$(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
|
|
||||||
@@ -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="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="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="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 -->
|
<!-- Start application messages -->
|
||||||
<string name="conn_establishing_title">Establishing Connection</string>
|
<string name="conn_establishing_title">Establishing Connection</string>
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<!-- Non-root application name -->
|
<!-- Non-root application name -->
|
||||||
|
<!-- FIXME: We should set extractNativeLibs=false but this breaks installation on the Fire TV 3 -->
|
||||||
<application android:label="Moonlight" />
|
<application android:label="Moonlight" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -2,5 +2,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<!-- Root application name -->
|
<!-- 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>
|
</manifest>
|
||||||
-5
@@ -6,7 +6,6 @@ import android.os.Looper;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.LimelightBuildProps;
|
|
||||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
|
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
@@ -186,10 +185,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
|||||||
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return LimelightBuildProps.ROOT_BUILD;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportDeviceNotRooted() {
|
private void reportDeviceNotRooted() {
|
||||||
activity.runOnUiThread(new Runnable() {
|
activity.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -32,3 +32,12 @@ This file serves to document some of the decoder errata when using MediaCodec ha
|
|||||||
|
|
||||||
11. Attempting to use reference picture invalidation at 1080p causes the decoder to crash on low-end Snapdragon SoCs. 720p is unaffected.
|
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
|
- 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)
|
||||||
+1
-1
Submodule moonlight-common updated: bb75967467...1480b1e7b8.
Reference in new issue
Block a user