Compare commits

..
32 Commits
Author SHA1 Message Date
Cameron Gutman 1ccbbdd4fb Version 8.10 2020-03-08 19:48:53 -07:00
Cameron Gutman 16cf37994d Only suppress duplicate d-pad events if the hat has received input. Fixes #796 2020-03-04 18:48:14 -08:00
Cameron Gutman 01e84624c2 Remove stale moonlight-common reference from settings.gradle 2020-03-03 00:13:48 -08:00
Cameron Gutman 939cd7cf70 Update OkHttp to 3.12.10 2020-03-02 22:49:44 -08:00
Cameron Gutman 4b11603035 Fix back button on Shield Portable and standardize external/internal classification 2020-03-02 22:47:47 -08:00
Cameron Gutman ca18b6b052 Update to AGP 3.6.1 2020-03-01 13:12:04 -08:00
Cameron Gutman 3d0d19e561 Pass-through back button on external devices that don't look like gamepads 2020-03-01 12:45:00 -08:00
Cameron Gutman ae463a8735 Emulated button combos must not be pressed with other buttons 2020-02-26 20:38:53 -08:00
Cameron Gutman 7e797829ae Also destroy the mouse emulation timer on device disconnect 2020-02-26 20:29:28 -08:00
Cameron Gutman 431ed6bc5d Cancel the mouse emulation timer when the stream ends 2020-02-26 20:18:11 -08:00
Cameron Gutman e9bb711c42 Add Start+Back+LB+RB combo for disconnecting the session 2020-02-26 19:54:53 -08:00
Cameron Gutman 623bc5c156 Fix check for gamepad buttons. Fixes #788 2020-02-26 19:19:43 -08:00
Cameron Gutman cfefef4619 Downgrade OkHTTP to 3.12.8 due to square/okhttp#5826 2020-02-25 22:40:17 -08:00
Cameron Gutman 4a9a881c1f Add missing else block 2020-02-25 22:26:52 -08:00
Cameron Gutman 13a06d585c Update dependencies 2020-02-25 20:49:30 -08:00
Cameron Gutman 1c8ad64da0 Only set KEY_FRAME_RATE on M+ to reduce compatibility risk 2020-02-25 20:24:18 -08:00
Cameron Gutman 1d8925de57 Fix NDK version in Travis CI build 2020-02-25 00:38:41 -08:00
Cameron Gutman 0eb7e779b8 Update Travis CI to build-tools-29.0.3 2020-02-25 00:25:15 -08:00
Cameron Gutman a4b86eefe2 Change errorCode from long to int to fix 32-bit platforms 2020-02-24 23:24:22 -08:00
Cameron Gutman 902a58bc70 Improve video decoder init failure message 2020-02-24 23:23:23 -08:00
Cameron Gutman a34a44f29a Fix crash on Android 5.0 and earlier 2020-02-24 22:05:26 -08:00
Cameron Gutman 454fe80172 Update Gradle and AGP for AS 3.6.0 2020-02-24 21:49:14 -08:00
Cameron Gutman 81b6a8a311 Set the vendor.qti-ext-dec-low-latency.enable Qualcomm vendor extension 2020-02-22 17:06:32 -08:00
Cameron Gutman 3011a5bad7 Use the unmodified FPS value when sending the launch request 2020-02-22 01:28:41 -08:00
Cameron Gutman dcb7be3acd Use the original FPS value for KEY_FRAME_RATE 2020-02-22 01:18:11 -08:00
Cameron Gutman 68a6b510b1 Set KEY_FRAME_RATE for devices where KEY_OPERATING_RATE silently fails 2020-02-22 01:05:26 -08:00
Cameron Gutman dca3e89303 Log configured MediaFormat and achievable FPS ranges 2020-02-22 01:04:18 -08:00
Cameron Gutman bae6fef588 Log the actual input and output formats 2020-02-21 22:02:37 -08:00
Cameron Gutman 37f65e43a5 Add error code on connection failure dialog 2020-02-21 22:01:12 -08:00
Cameron Gutman 8c910101c7 Fix Lint errors on API level 16 2020-02-19 23:53:44 -08:00
Cameron Gutman 112d9c41eb Use KEY_LOW_LATENCY to request low-latency decoding on Android R 2020-02-19 23:40:06 -08:00
Cameron Gutman c91d1097f6 Set preferMinimalPostProcessing on Android R 2020-02-19 23:29:37 -08:00
19 changed files with 302 additions and 77 deletions

No files matched your search

+5 -2
View File
@@ -8,8 +8,11 @@ android:
components: components:
- tools - tools
- platform-tools - platform-tools
- build-tools-29.0.1 - build-tools-29.0.3
- android-29 - android-29
before_install:
- sdkmanager --list
install: install:
- yes | sdkmanager "ndk-bundle" - yes | sdkmanager "ndk;20.0.5594570"
+6 -6
View File
@@ -7,8 +7,8 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionName "8.9" versionName "8.10"
versionCode = 211 versionCode = 215
} }
flavorDimensions "root" flavorDimensions "root"
@@ -114,10 +114,10 @@ android {
} }
dependencies { dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.62' implementation 'org.bouncycastle:bcprov-jdk15on:1.64'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.62' implementation 'org.bouncycastle:bcpkix-jdk15on:1.64'
implementation 'org.jcodec:jcodec:0.2.3' implementation 'org.jcodec:jcodec:0.2.3'
implementation 'com.squareup.okhttp3:okhttp:3.12.3' implementation 'com.squareup.okhttp3:okhttp:3.12.10'
implementation 'com.squareup.okio:okio:1.17.4' implementation 'com.squareup.okio:okio:1.17.5'
implementation 'org.jmdns:jmdns:3.5.5' implementation 'org.jmdns:jmdns:3.5.5'
} }
+36 -7
View File
@@ -73,6 +73,7 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@@ -398,6 +399,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Hopefully, we can get rid of this once someone comes up with a better way // 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. // to track the state of the pipeline and time frames.
int roundedRefreshRate = Math.round(displayRefreshRate); int roundedRefreshRate = Math.round(displayRefreshRate);
int chosenFrameRate = prefConfig.fps;
if (!prefConfig.disableFrameDrop || prefConfig.unlockFps) { if (!prefConfig.disableFrameDrop || prefConfig.unlockFps) {
if (Build.DEVICE.equals("coral") || Build.DEVICE.equals("flame")) { if (Build.DEVICE.equals("coral") || Build.DEVICE.equals("flame")) {
// HACK: Pixel 4 (XL) ignores the preferred display mode and lowers refresh rate, // HACK: Pixel 4 (XL) ignores the preferred display mode and lowers refresh rate,
@@ -423,8 +425,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Use the old rendering strategy on these broken devices // Use the old rendering strategy on these broken devices
decoderRenderer.enableLegacyFrameDropRendering(); decoderRenderer.enableLegacyFrameDropRendering();
} else { } else {
prefConfig.fps = roundedRefreshRate - 1; chosenFrameRate = roundedRefreshRate - 1;
LimeLog.info("Adjusting FPS target for screen to " + prefConfig.fps); LimeLog.info("Adjusting FPS target for screen to " + chosenFrameRate);
} }
} }
} }
@@ -436,7 +438,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
StreamConfiguration config = new StreamConfiguration.Builder() StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height) .setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps) .setLaunchRefreshRate(prefConfig.fps)
.setRefreshRate(chosenFrameRate)
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr)) .setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
.setBitrate(prefConfig.bitrate) .setBitrate(prefConfig.bitrate)
.setEnableSops(prefConfig.enableSops) .setEnableSops(prefConfig.enableSops)
@@ -592,6 +595,29 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
} }
// FIXME: Remove when Android R SDK is finalized
private static void setPreferMinimalPostProcessingWithReflection(WindowManager.LayoutParams windowLayoutParams, boolean isPreferred) {
// Build.VERSION.PREVIEW_SDK_INT was added in M
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && Build.VERSION.PREVIEW_SDK_INT == 0) {
// Don't attempt this reflection unless on Android R Developer Preview
return;
}
}
else {
return;
}
try {
Field field = windowLayoutParams.getClass().getDeclaredField("preferMinimalPostProcessing");
field.set(windowLayoutParams, isPreferred);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private float prepareDisplayForRendering() { private float prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay(); Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes(); WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
@@ -664,6 +690,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// whatever is currently in use. // whatever is currently in use.
} }
// Enable HDMI ALLM (game mode) on Android R
setPreferMinimalPostProcessingWithReflection(windowLayoutParams, true);
// Apply the display mode change // Apply the display mode change
getWindow().setAttributes(windowLayoutParams); getWindow().setAttributes(windowLayoutParams);
@@ -1373,7 +1402,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
@Override @Override
public void stageFailed(final String stage, final long errorCode) { public void stageFailed(final String stage, final int errorCode) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -1388,18 +1417,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// If video initialization failed and the surface is still valid, display extra information for the user // If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) { if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show(); Toast.makeText(Game.this, getResources().getText(R.string.video_decoder_init_failed), Toast.LENGTH_LONG).show();
} }
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title), Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg) + " " + stage, true); getResources().getString(R.string.conn_error_msg) + " " + stage +" (error "+errorCode+")", true);
} }
} }
}); });
} }
@Override @Override
public void connectionTerminated(final long errorCode) { public void connectionTerminated(final int errorCode) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -1,5 +1,6 @@
package com.limelight.binding.input; package com.limelight.binding.input;
import android.app.Activity;
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.UsbDevice;
@@ -51,7 +52,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>(); private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
private final NvConnection conn; private final NvConnection conn;
private final Context activityContext; private final Activity activityContext;
private final double stickDeadzone; private final double stickDeadzone;
private final InputDeviceContext defaultContext = new InputDeviceContext(); private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures; private final GameGestures gestures;
@@ -61,7 +62,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final PreferenceConfiguration prefConfig; private final PreferenceConfiguration prefConfig;
private short currentControllers, initialControllers; private short currentControllers, initialControllers;
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) { public ControllerHandler(Activity activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
this.activityContext = activityContext; this.activityContext = activityContext;
this.conn = conn; this.conn = conn;
this.gestures = gestures; this.gestures = gestures;
@@ -108,6 +109,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS; defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0; defaultContext.controllerNumber = (short) 0;
defaultContext.assignedControllerNumber = true; defaultContext.assignedControllerNumber = true;
defaultContext.external = false;
// Some devices (GPD XD) have a back button which sends input events // Some devices (GPD XD) have a back button which sends input events
// with device ID == 0. This hits the default context which would normally // with device ID == 0. This hits the default context which would normally
@@ -146,6 +148,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (context != null) { if (context != null) {
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")"); LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
releaseControllerNumber(context); releaseControllerNumber(context);
context.destroy();
inputDeviceContexts.remove(deviceId); inputDeviceContexts.remove(deviceId);
} }
} }
@@ -160,10 +163,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public void stop() { public void stop() {
for (int i = 0; i < inputDeviceContexts.size(); i++) { for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i); InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
deviceContext.destroy();
}
if (deviceContext.vibrator != null) { for (int i = 0; i < usbDeviceContexts.size(); i++) {
deviceContext.vibrator.cancel(); UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
} deviceContext.destroy();
} }
deviceVibrator.cancel(); deviceVibrator.cancel();
@@ -264,9 +269,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
InputDeviceContext devContext = (InputDeviceContext) context; InputDeviceContext devContext = (InputDeviceContext) context;
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned"); LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
if (devContext.name != null && if (!devContext.external) {
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
LimeLog.info("Built-in buttons hardcoded as controller 0"); LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0; context.controllerNumber = 0;
} }
@@ -327,6 +330,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.id = device.getControllerId(); context.id = device.getControllerId();
context.device = device; context.device = device;
context.external = true;
context.leftStickDeadzoneRadius = (float) stickDeadzone; context.leftStickDeadzoneRadius = (float) stickDeadzone;
context.rightStickDeadzoneRadius = (float) stickDeadzone; context.rightStickDeadzoneRadius = (float) stickDeadzone;
@@ -343,6 +347,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return true; return true;
} }
String deviceName = dev.getName();
if (deviceName.contains("gpio") || // This is the back button on Shield portable consoles
deviceName.contains("joy_key") || // These are the gamepad buttons on the Archos Gamepad 2
deviceName.contains("keypad") || // These are gamepad buttons on the XPERIA Play
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.01") || // Gamepad on Shield Portable
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.02")) // Gamepad on Shield Portable (?)
{
LimeLog.info(dev.getName()+" is internal by hardcoded mapping");
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q // Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
return dev.isExternal(); return dev.isExternal();
@@ -376,9 +391,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
} }
// Classify this device as a remote by name if it has no joystick axes // Classify this device as a remote by name if it has no joystick axes
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) == null && if (!hasJoystickAxes(dev) && devName.toLowerCase().contains("remote")) {
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) == null &&
devName.toLowerCase().contains("remote")) {
return true; return true;
} }
@@ -401,11 +414,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// Note that we are explicitly NOT excluding the current device we're examining here, // Note that we are explicitly NOT excluding the current device we're examining here,
// since the other gamepad buttons may be on our current device and that's fine. // since the other gamepad buttons may be on our current device and that's fine.
boolean[] keys = currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_A); if (currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT)[0]) {
if (keys[0]) {
foundInternalSelect = true; foundInternalSelect = true;
} }
if (keys[1]) {
// We don't check KEYCODE_BUTTON_A here, since the Shield Android TV has a
// virtual mouse device that claims to have KEYCODE_BUTTON_A. Instead, we rely
// on the SOURCE_GAMEPAD flag to be set on gamepad devices.
if (hasGamepadButtons(currentDev)) {
foundInternalGamepad = true; foundInternalGamepad = true;
} }
} }
@@ -417,8 +433,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable) // c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
return !foundInternalGamepad || foundInternalSelect; return !foundInternalGamepad || foundInternalSelect;
} }
else {
return false; // For external devices, we want to pass through the back button if the device
// has no gamepad axes or gamepad buttons.
return !hasJoystickAxes(dev) && !hasGamepadButtons(dev);
}
} }
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) { private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
@@ -434,6 +453,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.name = devName; context.name = devName;
context.id = dev.getId(); context.id = dev.getId();
context.external = isExternal(dev);
if (dev.getVibrator().hasVibrator()) { if (dev.getVibrator().hasVibrator()) {
context.vibrator = dev.getVibrator(); context.vibrator = dev.getVibrator();
@@ -888,18 +908,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
} }
} }
if (context.hatXAxis != -1 && context.hatYAxis != -1) { if (context.hatXAxis == -1 &&
switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
return 0;
}
}
else if (context.hatXAxis == -1 &&
context.hatYAxis == -1 && context.hatYAxis == -1 &&
/* FIXME: There's no good way to know for sure if xpad is bound /* FIXME: There's no good way to know for sure if xpad is bound
to this device, so we won't use the name to validate if these to this device, so we won't use the name to validate if these
@@ -1030,17 +1039,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG); context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) { if (hatX < -0.5) {
context.inputMap |= ControllerPacket.LEFT_FLAG; context.inputMap |= ControllerPacket.LEFT_FLAG;
context.hatXAxisUsed = true;
} }
else if (hatX > 0.5) { else if (hatX > 0.5) {
context.inputMap |= ControllerPacket.RIGHT_FLAG; context.inputMap |= ControllerPacket.RIGHT_FLAG;
context.hatXAxisUsed = true;
} }
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG); context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) { if (hatY < -0.5) {
context.inputMap |= ControllerPacket.UP_FLAG; context.inputMap |= ControllerPacket.UP_FLAG;
context.hatYAxisUsed = true;
} }
else if (hatY > 0.5) { else if (hatY > 0.5) {
context.inputMap |= ControllerPacket.DOWN_FLAG; context.inputMap |= ControllerPacket.DOWN_FLAG;
context.hatYAxisUsed = true;
} }
} }
@@ -1255,15 +1268,31 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap &= ~ControllerPacket.BACK_FLAG; context.inputMap &= ~ControllerPacket.BACK_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.LEFT_FLAG; context.inputMap &= ~ControllerPacket.LEFT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.RIGHT_FLAG; context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.UP_FLAG; context.inputMap &= ~ControllerPacket.UP_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.DOWN_FLAG; context.inputMap &= ~ControllerPacket.DOWN_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
@@ -1347,6 +1376,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
} }
sendControllerInputPacket(context); sendControllerInputPacket(context);
if (context.pendingExit && context.inputMap == 0) {
// All buttons from the quit combo are lifted. Finish the activity now.
activityContext.finish();
}
return true; return true;
} }
@@ -1377,15 +1412,31 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap |= ControllerPacket.BACK_FLAG; context.inputMap |= ControllerPacket.BACK_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.LEFT_FLAG; context.inputMap |= ControllerPacket.LEFT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.RIGHT_FLAG; context.inputMap |= ControllerPacket.RIGHT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.UP_FLAG; context.inputMap |= ControllerPacket.UP_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.DOWN_FLAG; context.inputMap |= ControllerPacket.DOWN_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
@@ -1431,9 +1482,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return false; return false;
} }
// Start+Back+LB+RB is the quit combo
if (context.inputMap == (ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG |
ControllerPacket.LB_FLAG | ControllerPacket.RB_FLAG)) {
// Wait for the combo to lift and then finish the activity
context.pendingExit = true;
}
// Start+LB acts like select for controllers with one button // Start+LB acts like select for controllers with one button
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 && if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
((context.inputMap & ControllerPacket.LB_FLAG) != 0 || (context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS)) SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{ {
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG); context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
@@ -1443,10 +1501,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
} }
// We detect select+start or start+RB as the special button combo // We detect select+start or start+RB as the special button combo
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 || if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG) ||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) || context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) && (context.inputMap == ControllerPacket.PLAY_FLAG &&
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0) SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{ {
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG); context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
@@ -1454,7 +1512,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL; context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
} }
// We don't need to send repeat key down events, but the platform // We don't need to send repeat key down events, but the platform
// sends us events that claim to be repeats but they're from different // sends us events that claim to be repeats but they're from different
// devices, so we just send them all and deal with some duplicates. // devices, so we just send them all and deal with some duplicates.
@@ -1525,6 +1582,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if (context != null) { if (context != null) {
LimeLog.info("Removed controller: "+controller.getControllerId()); LimeLog.info("Removed controller: "+controller.getControllerId());
releaseControllerNumber(context); releaseControllerNumber(context);
context.destroy();
usbDeviceContexts.remove(controller.getControllerId()); usbDeviceContexts.remove(controller.getControllerId());
} }
} }
@@ -1537,6 +1595,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
class GenericControllerContext { class GenericControllerContext {
public int id; public int id;
public boolean external;
public float leftStickDeadzoneRadius; public float leftStickDeadzoneRadius;
public float rightStickDeadzoneRadius; public float rightStickDeadzoneRadius;
@@ -1557,6 +1616,13 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public boolean mouseEmulationActive; public boolean mouseEmulationActive;
public Timer mouseEmulationTimer; public Timer mouseEmulationTimer;
public short mouseEmulationLastInputMap; public short mouseEmulationLastInputMap;
public void destroy() {
if (mouseEmulationTimer != null) {
mouseEmulationTimer.cancel();
mouseEmulationTimer = null;
}
}
} }
class InputDeviceContext extends GenericControllerContext { class InputDeviceContext extends GenericControllerContext {
@@ -1576,6 +1642,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public int hatXAxis = -1; public int hatXAxis = -1;
public int hatYAxis = -1; public int hatYAxis = -1;
public boolean hatXAxisUsed, hatYAxisUsed;
public boolean isNonStandardDualShock4; public boolean isNonStandardDualShock4;
public boolean usesLinuxGamepadStandardFaceButtons; public boolean usesLinuxGamepadStandardFaceButtons;
@@ -1585,6 +1652,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public boolean modeIsSelect; public boolean modeIsSelect;
public boolean ignoreBack; public boolean ignoreBack;
public boolean hasJoystickAxes; public boolean hasJoystickAxes;
public boolean pendingExit;
public int emulatingButtonFlags = 0; public int emulatingButtonFlags = 0;
@@ -1597,9 +1665,25 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public long lastRbUpTime = 0; public long lastRbUpTime = 0;
public long startDownTime = 0; public long startDownTime = 0;
@Override
public void destroy() {
super.destroy();
if (vibrator != null) {
vibrator.cancel();
}
}
} }
class UsbDeviceContext extends GenericControllerContext { class UsbDeviceContext extends GenericControllerContext {
public AbstractController device; public AbstractController device;
@Override
public void destroy() {
super.destroy();
// Nothing for now
}
} }
} }
@@ -45,6 +45,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private Thread rendererThread; private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4; private boolean needsSpsBitstreamFixup, isExynos4;
private boolean adaptivePlayback, directSubmit; private boolean adaptivePlayback, directSubmit;
private boolean lowLatency;
private boolean constrainedHighProfile; private boolean constrainedHighProfile;
private boolean refFrameInvalidationAvc, refFrameInvalidationHevc; private boolean refFrameInvalidationAvc, refFrameInvalidationHevc;
private boolean refFrameInvalidationActive; private boolean refFrameInvalidationActive;
@@ -60,6 +61,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private boolean legacyFrameDropRendering = false; private boolean legacyFrameDropRendering = false;
private PerfOverlayListener perfListener; private PerfOverlayListener perfListener;
private MediaFormat inputFormat;
private MediaFormat outputFormat;
private MediaFormat configuredFormat;
private boolean needsBaselineSpsHack; private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps; private SeqParameterSet savedSps;
@@ -161,7 +166,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// shared between AVC and HEVC decoders on the same device. // shared between AVC and HEVC decoders on the same device.
if (avcDecoder != null) { if (avcDecoder != null) {
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName()); directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName());
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder);
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height); refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName()); refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
@@ -264,6 +268,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
refFrameInvalidationActive = refFrameInvalidationAvc; refFrameInvalidationActive = refFrameInvalidationAvc;
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(avcDecoder, mimeType);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder, mimeType);
} }
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) { else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
mimeType = "video/hevc"; mimeType = "video/hevc";
@@ -275,6 +282,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
} }
refFrameInvalidationActive = refFrameInvalidationHevc; refFrameInvalidationActive = refFrameInvalidationHevc;
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(hevcDecoder, mimeType);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(hevcDecoder, mimeType);
} }
else { else {
// Unknown format // Unknown format
@@ -293,6 +303,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height); MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
// Avoid setting KEY_FRAME_RATE on Lollipop and earlier to reduce compatibility risk
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// We use prefs.fps instead of redrawRate here because the low latency hack in Game.java
// may leave us with an odd redrawRate value like 59 or 49 which might cause the decoder
// to puke. To be safe, we'll use the unmodified value.
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, prefs.fps);
}
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices // Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
// so we don't fill these pre-KitKat // so we don't fill these pre-KitKat
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
@@ -300,7 +318,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height); videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (lowLatency) {
videoFormat.setInteger(MediaCodecHelper.KEY_LOW_LATENCY, 1);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Set the Qualcomm vendor low latency extension if the Android R option is unavailable
if (MediaCodecHelper.decoderSupportsQcomVendorLowLatency(selectedDecoderName)) {
// MediaCodec supports vendor-defined format keys using the "vendor.<extension name>.<parameter name>" syntax.
// These allow access to functionality that is not exposed through documented MediaFormat.KEY_* values.
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/common/inc/vidc_vendor_extensions.h;l=67
//
// Examples of Qualcomm's vendor extensions for Snapdragon 845:
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/vdec/src/omx_vdec_extensions.hpp
// https://cs.android.com/android/_/android/platform/hardware/qcom/sm8150/media/+/0621ceb1c1b19564999db8293574a0e12952ff6c
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
}
// Operate at maximum rate to lower latency as much as possible on // Operate at maximum rate to lower latency as much as possible on
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime) // some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
// but that will actually result in the decoder crashing if it can't satisfy // but that will actually result in the decoder crashing if it can't satisfy
@@ -308,8 +341,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE); videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
} }
configuredFormat = videoFormat;
LimeLog.info("Configuring with format: "+configuredFormat);
try { try {
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0); videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// This will contain the actual accepted input format attributes
inputFormat = videoDecoder.getInputFormat();
LimeLog.info("Input format: "+inputFormat);
}
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -334,6 +377,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
legacyInputBuffers = videoDecoder.getInputBuffers(); legacyInputBuffers = videoDecoder.getInputBuffers();
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return -5; return -5;
@@ -445,7 +489,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
break; break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed"); LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); outputFormat = videoDecoder.getOutputFormat();
LimeLog.info("New output format: " + outputFormat);
break; break;
default: default:
break; break;
@@ -1013,17 +1058,29 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) { private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) {
String str = ""; String str = "";
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "Format: "+String.format("%x", renderer.videoFormat)+"\n"; str += "Format: "+String.format("%x", renderer.videoFormat)+"\n";
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n"; str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n"; str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths(); Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
str += "AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper()+"\n"; str += "AVC supported width range: "+avcWidthRange+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Range<Double> avcFpsRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
str += "AVC achievable FPS range: "+avcFpsRange+"\n";
}
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths(); Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
str += "HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper()+"\n"; str += "HEVC supported width range: "+hevcWidthRange+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Range<Double> hevcFpsRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
str += "HEVC achievable FPS range: "+hevcFpsRange+"\n";
}
} }
str += "Configured format: "+renderer.configuredFormat+"\n";
str += "Input format: "+renderer.inputFormat+"\n";
str += "Output format: "+renderer.outputFormat+"\n";
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n"; str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
str += "GL Renderer: "+renderer.glRenderer+"\n"; str += "GL Renderer: "+renderer.glRenderer+"\n";
str += "Build fingerprint: "+Build.FINGERPRINT+"\n"; str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
@@ -1031,7 +1088,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n"; str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n"; str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n"; str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n";
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n"; str += "Low latency mode: "+renderer.lowLatency+"\n";
str += "FPS target: "+renderer.refreshRate+"\n"; str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n"; str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n"; str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
@@ -38,6 +38,11 @@ public class MediaCodecHelper {
private static final List<String> refFrameInvalidationHevcPrefixes; private static final List<String> refFrameInvalidationHevcPrefixes;
private static final List<String> blacklisted49FpsDecoderPrefixes; private static final List<String> blacklisted49FpsDecoderPrefixes;
private static final List<String> blacklisted59FpsDecoderPrefixes; private static final List<String> blacklisted59FpsDecoderPrefixes;
private static final List<String> qualcommDecoderPrefixes;
// FIXME: Remove when Android R SDK is finalized
public static final String FEATURE_LowLatency = "low-latency";
public static final String KEY_LOW_LATENCY = "low-latency";
private static boolean isLowEndSnapdragon = false; private static boolean isLowEndSnapdragon = false;
private static boolean initialized = false; private static boolean initialized = false;
@@ -189,6 +194,13 @@ public class MediaCodecHelper {
} }
} }
static {
qualcommDecoderPrefixes = new LinkedList<>();
qualcommDecoderPrefixes.add("omx.qcom");
qualcommDecoderPrefixes.add("c2.qti");
}
private static boolean isPowerVR(String glRenderer) { private static boolean isPowerVR(String glRenderer) {
return glRenderer.toLowerCase().contains("powervr"); return glRenderer.toLowerCase().contains("powervr");
} }
@@ -330,7 +342,24 @@ public class MediaCodecHelper {
return System.nanoTime() / 1000000L; return System.nanoTime() / 1000000L;
} }
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) { public static boolean decoderSupportsLowLatency(MediaCodecInfo decoderInfo, String mimeType) {
// KitKat added CodecCapabilities.isFeatureSupported()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
if (decoderInfo.getCapabilitiesForType(mimeType).isFeatureSupported(FEATURE_LowLatency)) {
LimeLog.info("Low latency decoding mode supported (FEATURE_LowLatency)");
return true;
}
} catch (Exception e) {
// Tolerate buggy codecs
e.printStackTrace();
}
}
return false;
}
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo, String mimeType) {
// 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) {
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) { if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
@@ -339,7 +368,7 @@ public class MediaCodecHelper {
} }
try { try {
if (decoderInfo.getCapabilitiesForType("video/avc"). if (decoderInfo.getCapabilitiesForType(mimeType).
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback)) isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
{ {
// This will make getCapabilities() return that adaptive playback is supported // This will make getCapabilities() return that adaptive playback is supported
@@ -348,12 +377,20 @@ public class MediaCodecHelper {
} }
} catch (Exception e) { } catch (Exception e) {
// Tolerate buggy codecs // Tolerate buggy codecs
e.printStackTrace();
} }
} }
return false; return false;
} }
public static boolean decoderSupportsQcomVendorLowLatency(String decoderName) {
// MediaCodec vendor extension support was introduced in Android 8.0:
// https://cs.android.com/android/_/android/platform/frameworks/av/+/01c10f8cdcd58d1e7025f426a72e6e75ba5d7fc2
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
isDecoderInList(qualcommDecoderPrefixes, decoderName);
}
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) { public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
return isDecoderInList(constrainedHighProfilePrefixes, decoderName); return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
} }
@@ -17,7 +17,6 @@ public class ConnectionContext {
public String serverGfeVersion; public String serverGfeVersion;
public int negotiatedWidth, negotiatedHeight; public int negotiatedWidth, negotiatedHeight;
public int negotiatedFps;
public boolean negotiatedHdr; public boolean negotiatedHdr;
public int videoCapabilities; public int videoCapabilities;
@@ -121,13 +121,11 @@ public class NvConnection {
// Lower resolution to 1080p // Lower resolution to 1080p
context.negotiatedWidth = 1920; context.negotiatedWidth = 1920;
context.negotiatedHeight = 1080; context.negotiatedHeight = 1080;
context.negotiatedFps = context.streamConfig.getRefreshRate();
} }
else { else {
// Take what the client wanted // Take what the client wanted
context.negotiatedWidth = context.streamConfig.getWidth(); context.negotiatedWidth = context.streamConfig.getWidth();
context.negotiatedHeight = context.streamConfig.getHeight(); context.negotiatedHeight = context.streamConfig.getHeight();
context.negotiatedFps = context.streamConfig.getRefreshRate();
} }
// //
@@ -263,7 +261,7 @@ public class NvConnection {
int ret = MoonBridge.startConnection(context.serverAddress, int ret = MoonBridge.startConnection(context.serverAddress,
context.serverAppVersion, context.serverGfeVersion, context.serverAppVersion, context.serverGfeVersion,
context.negotiatedWidth, context.negotiatedHeight, context.negotiatedWidth, context.negotiatedHeight,
context.negotiatedFps, context.streamConfig.getBitrate(), context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
context.streamConfig.getMaxPacketSize(), context.streamConfig.getMaxPacketSize(),
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(), context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
context.streamConfig.getHevcSupported(), context.streamConfig.getHevcSupported(),
@@ -3,10 +3,10 @@ package com.limelight.nvstream;
public interface NvConnectionListener { public interface NvConnectionListener {
void stageStarting(String stage); void stageStarting(String stage);
void stageComplete(String stage); void stageComplete(String stage);
void stageFailed(String stage, long errorCode); void stageFailed(String stage, int errorCode);
void connectionStarted(); void connectionStarted();
void connectionTerminated(long errorCode); void connectionTerminated(int errorCode);
void connectionStatusUpdate(int connectionStatus); void connectionStatusUpdate(int connectionStatus);
void displayMessage(String message); void displayMessage(String message);
@@ -19,6 +19,7 @@ public class StreamConfiguration {
private NvApp app; private NvApp app;
private int width, height; private int width, height;
private int refreshRate; private int refreshRate;
private int launchRefreshRate;
private int clientRefreshRateX100; private int clientRefreshRateX100;
private int bitrate; private int bitrate;
private boolean sops; private boolean sops;
@@ -57,6 +58,11 @@ public class StreamConfiguration {
config.refreshRate = refreshRate; config.refreshRate = refreshRate;
return this; return this;
} }
public StreamConfiguration.Builder setLaunchRefreshRate(int refreshRate) {
config.launchRefreshRate = refreshRate;
return this;
}
public StreamConfiguration.Builder setBitrate(int bitrate) { public StreamConfiguration.Builder setBitrate(int bitrate) {
config.bitrate = bitrate; config.bitrate = bitrate;
@@ -147,6 +153,7 @@ public class StreamConfiguration {
this.width = 1280; this.width = 1280;
this.height = 720; this.height = 720;
this.refreshRate = 60; this.refreshRate = 60;
this.launchRefreshRate = 60;
this.bitrate = 10000; this.bitrate = 10000;
this.maxPacketSize = 1024; this.maxPacketSize = 1024;
this.remote = STREAM_CFG_AUTO; this.remote = STREAM_CFG_AUTO;
@@ -170,6 +177,10 @@ public class StreamConfiguration {
public int getRefreshRate() { public int getRefreshRate() {
return refreshRate; return refreshRate;
} }
public int getLaunchRefreshRate() {
return launchRefreshRate;
}
public int getBitrate() { public int getBitrate() {
return bitrate; return bitrate;
@@ -624,7 +624,7 @@ public class NvHTTP {
// Using an FPS value over 60 causes SOPS to default to 720p60, // Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 60 when starting. This won't impact our ability // so force it to 60 when starting. This won't impact our ability
// to get > 60 FPS while actually streaming though. // to get > 60 FPS while actually streaming though.
int fps = context.negotiatedFps > 60 ? 60 : context.negotiatedFps; int fps = context.streamConfig.getLaunchRefreshRate() > 60 ? 60 : context.streamConfig.getLaunchRefreshRate();
// Using an unsupported resolution (not 720p, 1080p, or 4K) causes // Using an unsupported resolution (not 720p, 1080p, or 4K) causes
// GFE to force SOPS to 720p60. This is fine for < 720p resolutions like // GFE to force SOPS to 720p60. This is fine for < 720p resolutions like
@@ -129,7 +129,7 @@ public class MoonBridge {
} }
} }
public static void bridgeClStageFailed(int stage, long errorCode) { public static void bridgeClStageFailed(int stage, int errorCode) {
if (connectionListener != null) { if (connectionListener != null) {
connectionListener.stageFailed(getStageName(stage), errorCode); connectionListener.stageFailed(getStageName(stage), errorCode);
} }
@@ -141,7 +141,7 @@ public class MoonBridge {
} }
} }
public static void bridgeClConnectionTerminated(long errorCode) { public static void bridgeClConnectionTerminated(int errorCode) {
if (connectionListener != null) { if (connectionListener != null) {
connectionListener.connectionTerminated(errorCode); connectionListener.connectionTerminated(errorCode);
} }
+5 -5
View File
@@ -87,9 +87,9 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
BridgeArPlaySampleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArPlaySample", "([S)V"); BridgeArPlaySampleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArPlaySample", "([S)V");
BridgeClStageStartingMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageStarting", "(I)V"); BridgeClStageStartingMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageStarting", "(I)V");
BridgeClStageCompleteMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageComplete", "(I)V"); BridgeClStageCompleteMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageComplete", "(I)V");
BridgeClStageFailedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageFailed", "(IJ)V"); BridgeClStageFailedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageFailed", "(II)V");
BridgeClConnectionStartedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStarted", "()V"); BridgeClConnectionStartedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStarted", "()V");
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(J)V"); BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(I)V");
BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V"); BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V");
BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V"); BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V");
} }
@@ -311,7 +311,7 @@ void BridgeClStageComplete(int stage) {
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageCompleteMethod, stage); (*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageCompleteMethod, stage);
} }
void BridgeClStageFailed(int stage, long errorCode) { void BridgeClStageFailed(int stage, int errorCode) {
JNIEnv* env = GetThreadEnv(); JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) { if ((*env)->ExceptionCheck(env)) {
@@ -328,10 +328,10 @@ void BridgeClConnectionStarted(void) {
return; return;
} }
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStartedMethod, NULL); (*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStartedMethod);
} }
void BridgeClConnectionTerminated(long errorCode) { void BridgeClConnectionTerminated(int errorCode) {
JNIEnv* env = GetThreadEnv(); JNIEnv* env = GetThreadEnv();
if ((*env)->ExceptionCheck(env)) { if ((*env)->ExceptionCheck(env)) {
+1
View File
@@ -60,6 +60,7 @@
<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> <string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
<string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string> <string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string>
<string name="video_decoder_init_failed">Video decoder failed to initialize. Your device may not support the selected resolution or frame rate.</string>
<!-- Start application messages --> <!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string> <string name="conn_establishing_title">Establishing Connection</string>
+1 -1
View File
@@ -5,7 +5,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.3' classpath 'com.android.tools.build:gradle:3.6.1'
} }
} }
@@ -0,0 +1,6 @@
- Latency optimizations for Android R DP1
- Video decoder optimizations
- Added Start+Back+LB+RB combo to disconnect the stream
- Fixed back button on some Android TV remotes
- Fixed d-pad on gamepads that expose non-functional hat axes
- Fixed unexpected mouse input after using gamepad mouse emulation mode
+2 -2
View File
@@ -1,6 +1,6 @@
#Tue Aug 20 11:37:45 PDT 2019 #Mon Feb 24 12:32:24 PST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
+1 -1
View File
@@ -1 +1 @@
include ':app', ':moonlight-common' include ':app'