Compare commits

..

67 Commits

Author SHA1 Message Date
Cameron Gutman aede16c85c Version 10.9 2022-10-07 22:02:56 -05:00
Cameron Gutman 61a82e6394 Merge remote-tracking branch 'origin/weblate' 2022-10-07 21:55:19 -05:00
Sargon-Isa 5a92925d6a Translated using Weblate (German)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/de/
2022-10-08 04:54:57 +02:00
Cameron Gutman fe697c918f Update moonlight-common-c with speculative RFI support 2022-10-07 21:54:00 -05:00
Cameron Gutman bc57a285ce Fix unescaped character 2022-10-04 20:03:10 -05:00
Cameron Gutman 85d8943b64 Merge remote-tracking branch 'origin/weblate' 2022-10-04 19:56:52 -05:00
Cameron Gutman aa6c32968b Add a special termination message for ML_ERROR_FRAME_CONVERSION 2022-10-04 19:51:49 -05:00
Cameron Gutman ad1808fb4e Update moonlight-common-c with further fixes for GFE 3.26 2022-10-04 19:50:49 -05:00
Kamil Szyc 576610e4c3 Translated using Weblate (Polish)
Currently translated at 1.7% (4 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pl/
2022-10-04 12:24:06 +02:00
Martin Dimitrov ace2266f14 Translated using Weblate (Bulgarian)
Currently translated at 59.5% (134 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/bg/
2022-10-04 12:24:06 +02:00
Alexandru-Marian Buza 41cedfa6ec Fix requestMetaKeyEvent for Samsung devices with android 10+ (#1134)
Co-authored-by: Alexandru Buza <abuza@iqnox.com>
2022-10-03 22:50:04 -05:00
Cameron Gutman d46fab33b3 Enable HEVC RFI for Exynos decoders 2022-10-03 22:23:59 -05:00
Cameron Gutman 585dc45595 Enable RFI for HEVC on Qualcomm and Nvidia decoders 2022-10-03 21:33:05 -05:00
Cameron Gutman c3c9354a00 Update moonlight-common-c to support reliable RFI for HEVC 2022-10-03 21:32:11 -05:00
Cameron Gutman bdc8d08e65 Switch back to AGP 7.2.2
AGP 7.3.0 produces invalid bytecode for ControllerHandler, causing dex validation errors on Android Jelly Bean and KitKat

Fixes #1132
2022-10-03 21:30:01 -05:00
Cameron Gutman 9c792d3272 Adjust RendererException text to attempt to parse correctly in Google Play App Vitals 2022-10-03 21:28:37 -05:00
Cameron Gutman 23bc4daf9f Refactor input event handling in the Game activity 2022-10-03 21:25:43 -05:00
Kamil Szyc fd85ca2004 Added translation using Weblate (Polish) 2022-10-03 11:42:50 +02:00
Martin Dimitrov aadf88add1 Added translation using Weblate (Bulgarian) 2022-10-02 19:24:17 +02:00
sanhoe f14ce61ee3 Translated using Weblate (Korean)
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2022-09-29 12:16:23 +02:00
Cameron Gutman 539daf5789 Don't adjust maxBytesPerPicDenom and maxBitsPerMbDenom on newer devices 2022-09-23 21:27:27 -05:00
Cameron Gutman e8ea2a8ec1 Version 10.8.4 2022-09-22 23:17:58 -05:00
Cameron Gutman 9ed3b3a9df Fixed streaming on certain devices with GFE 3.26 2022-09-22 23:16:40 -05:00
Cameron Gutman 12487553de Version 10.8.2 2022-09-22 21:57:42 -05:00
Cameron Gutman 9c1a618b4a Fix stuck analog stick when a touch event is cancelled
This can happen if a stylus hover event is received while touching an OSC element
2022-09-21 01:11:45 -05:00
Cameron Gutman ac0e784417 Make StreamView transparent to touch events and handle everything in the background view
This is much simpler than trying to play games with touch handling between 2 views
2022-09-21 01:07:49 -05:00
Cameron Gutman 48cab6b203 Allow multi-finger gestures and absolute motion to pass seamlessly between the StreamView and background view 2022-09-21 00:21:43 -05:00
Cameron Gutman e1c0472069 Properly split touch events between regions outside the StreamView and the OSC
This restores the ability to use area outside the StreamView for the virtual trackpad and adds the ability to use OSC and the non-StreamView region for input at the same time.

Fixes #1129
2022-09-20 22:29:54 -05:00
Cameron Gutman 2c498ce707 Throw a RendererException instead of a bare IllegalStateException upon codec recovery failure 2022-09-20 21:43:35 -05:00
Cameron Gutman bc483edb29 Interrupt codec recovery when stopping the decoder 2022-09-18 18:53:37 -05:00
Cameron Gutman 9762f4c412 Only throw the codec exception on the last configuration attempt 2022-09-18 18:47:01 -05:00
Cameron Gutman 5bfce88fc5 Fix recovery timeout if no output frames are being received 2022-09-18 18:37:33 -05:00
Cameron Gutman 94ef66994d Trigger the decoder crash dialog if all recovery attempts fail 2022-09-18 18:29:45 -05:00
Cameron Gutman 257c29daca Improve handling of concurrent recoverable and non-recoverable errors and surface loss 2022-09-18 18:25:29 -05:00
Cameron Gutman 173483eb84 Only catch IllegalStateException or subclasses 2022-09-18 17:42:37 -05:00
Cameron Gutman 06099b2663 Only try to recover from CodecExceptions or IllegalStateExceptions 2022-09-18 00:20:41 -05:00
Cameron Gutman 33c1f0a71c Fix decoding crash if encoder didn't send VUI parameters 2022-09-18 00:04:29 -05:00
Cameron Gutman a3d78f1d80 Merge remote-tracking branch 'origin/weblate' 2022-09-17 23:32:55 -05:00
Cameron Gutman c573d213f8 Allow FFmpeg decoder on Waydroid 2022-09-17 14:51:03 -05:00
Cameron Gutman c72707aef9 Don't begin codec recovery if stopping 2022-09-17 13:52:22 -05:00
Cameron Gutman 313ef06c86 Only exclude touch events from non-view processing
Mouse events that go out of the StreamView area are okay
2022-09-17 13:36:44 -05:00
Cameron Gutman 6b79340c15 Don't handle motion events outside of Views to avoid spurious stream input while using OSC 2022-09-17 13:34:14 -05:00
Cameron Gutman d9a5b29372 Fix OSC handling of touches outside the StreamView 2022-09-17 13:32:40 -05:00
Cameron Gutman d2b0e093fc Reduce power by avoiding resends when OSC state is not changing 2022-09-17 13:07:52 -05:00
Cameron Gutman 945e563912 Switch to a Handler for gamepad mouse emulation 2022-09-17 12:55:15 -05:00
Cameron Gutman a7efa379eb Switch to a Handler for OSC retransmission 2022-09-16 18:21:56 -05:00
Cameron Gutman d04df4ebe5 Fix D-Pad buttons not releasing until all D-Pad input has ceased 2022-09-16 17:41:52 -05:00
Cameron Gutman 2a2c84ef3a Implement fallbacks for a failed codec restart or reset 2022-09-16 03:48:49 -05:00
Cameron Gutman bc97db893a Allow recovery of IllegalStateExceptions for older versions of Android 2022-09-16 03:28:57 -05:00
Cameron Gutman f216834df7 Limit the number of codec recovery attempts 2022-09-16 03:27:22 -05:00
Cameron Gutman be25a7d594 Fix a number of bugs in new codec recovery code 2022-09-16 03:19:36 -05:00
Cameron Gutman 10f43e8024 Try to adjust decoder exception to comply with Google Play crash message filtering 2022-09-16 00:32:34 -05:00
Cameron Gutman bbb3e8d071 Only catch RuntimeExceptions for decoders to avoid eating important exceptions 2022-09-16 00:26:02 -05:00
Cameron Gutman 4c3af35156 Update AGP to 7.3.0 2022-09-16 00:09:22 -05:00
Cameron Gutman 8656228014 Break out of wait on InterruptedException 2022-09-16 00:09:09 -05:00
Cameron Gutman 03f9ea8435 Use Handlers instead of Timers for one-shot events 2022-09-16 00:08:48 -05:00
Cameron Gutman 9cf27d8fb1 Don't throw exceptions during codec recovery 2022-09-15 02:16:24 -05:00
Cameron Gutman d1b24ea6af Consolidate touch tracking timers 2022-09-15 02:05:40 -05:00
Cameron Gutman b07ffbde29 Consolidate OSC timers 2022-09-15 01:59:29 -05:00
Cameron Gutman 1673236940 Abort if the decoder doesn't recover within 5 seconds 2022-09-15 01:37:10 -05:00
Cameron Gutman 06861a2d17 Add support for recovering from non-transient CodecExceptions 2022-09-15 01:15:15 -05:00
Cameron Gutman ef7ac62f97 Improve handling of transient CodecExceptions 2022-09-15 00:08:06 -05:00
Cameron Gutman 245a9f2751 Try a new input buffer if getInputBuffer() returns null 2022-09-14 23:54:07 -05:00
Cameron Gutman 1d38f158b5 Fix crash after the next fetchNextInputBuffer() if getInputBuffer() failed previously 2022-09-14 23:49:49 -05:00
Jorys Paulin 62a526854d Translated using Weblate (French)
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-09-14 15:22:45 +02:00
Jen Kung-chih 3dda940c92 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-09-13 08:19:00 +02:00
Howard Wu ab77c4720d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-09-13 08:18:59 +02:00
27 changed files with 903 additions and 469 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ android {
minSdk 16
targetSdk 33
versionName "10.8.1"
versionCode = 288
versionName "10.9"
versionCode = 296
// Generate native debug symbols to allow Google Play to symbolicate our native crashes
ndk.debugSymbolLevel = 'FULL'
+74 -32
View File
@@ -87,10 +87,9 @@ import java.util.Locale;
public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
PerfOverlayListener, UsbDriverService.UsbDriverStateListener
{
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
PerfOverlayListener, UsbDriverService.UsbDriverStateListener, View.OnKeyListener {
private int lastButtonState = 0;
// Only 2 touches are supported
@@ -232,12 +231,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
// Listen for events on the game surface
// Listen for non-touch events on the game surface
streamView = findViewById(R.id.surfaceView);
streamView.setOnGenericMotionListener(this);
streamView.setOnTouchListener(this);
streamView.setOnKeyListener(this);
streamView.setInputCallbacks(this);
// Listen for touch events on the background touch view to enable trackpad mode
// to work on areas outside of the StreamView itself. We use a separate View
// for this rather than just handling it at the Activity level, because that
// allows proper touch splitting, which the OSC relies upon.
View backgroundTouchView = findViewById(R.id.backgroundTouchView);
backgroundTouchView.setOnTouchListener(this);
boolean needsInputBatching = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Request unbuffered input event dispatching for all input classes we handle here.
@@ -250,6 +256,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
InputDevice.SOURCE_CLASS_POSITION | // Touchpads
InputDevice.SOURCE_CLASS_TRACKBALL // Mice (pointer capture)
);
backgroundTouchView.requestUnbufferedDispatch(
InputDevice.SOURCE_CLASS_BUTTON | // Keyboards
InputDevice.SOURCE_CLASS_JOYSTICK | // Gamepads
InputDevice.SOURCE_CLASS_POINTER | // Touchscreens and mice (w/o pointer capture)
InputDevice.SOURCE_CLASS_POSITION | // Touchpads
InputDevice.SOURCE_CLASS_TRACKBALL // Mice (pointer capture)
);
// Since the OS isn't going to batch for us, we have to batch mouse events to
// avoid triggering a bug in GeForce Experience that can lead to massive latency.
@@ -263,9 +276,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// The view must be focusable for pointer capture to work.
streamView.setFocusable(true);
streamView.setDefaultFocusHighlightEnabled(false);
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
@Override
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
@@ -686,7 +696,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (manager != null) {
Class<?>[] parameterTypes = new Class<?>[2];
parameterTypes[0] = String.class;
parameterTypes[0] = ComponentName.class;
parameterTypes[1] = boolean.class;
Method requestMetaKeyEventMethod = semWindowManager.getDeclaredMethod("requestMetaKeyEvent", parameterTypes);
requestMetaKeyEventMethod.invoke(manager, this.getComponentName(), enabled);
@@ -1574,15 +1584,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
if (view == null && !prefConfig.touchscreenTrackpad) {
// Absolute touch events should be dropped outside our view.
return true;
// If this is the parent view, we'll offset our coordinates to appear as if they
// are relative to the StreamView like our StreamView touch events are.
float xOffset, yOffset;
if (view != streamView && !prefConfig.touchscreenTrackpad) {
xOffset = -streamView.getX();
yOffset = -streamView.getY();
}
else {
xOffset = 0.f;
yOffset = 0.f;
}
int actionIndex = event.getActionIndex();
int eventX = (int)event.getX(actionIndex);
int eventY = (int)event.getY(actionIndex);
int eventX = (int)(event.getX(actionIndex) + xOffset);
int eventY = (int)(event.getY(actionIndex) + yOffset);
// Special handling for 3 finger gesture
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN &&
@@ -1637,7 +1654,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
// The original secondary touch now becomes primary
context.touchDownEvent((int)event.getX(1), (int)event.getY(1), event.getEventTime(), false);
context.touchDownEvent(
(int)(event.getX(1) + xOffset),
(int)(event.getY(1) + yOffset),
event.getEventTime(), false);
}
break;
case MotionEvent.ACTION_MOVE:
@@ -1650,8 +1670,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
{
aTouchContextMap.touchMoveEvent(
(int)event.getHistoricalX(aTouchContextMap.getActionIndex(), i),
(int)event.getHistoricalY(aTouchContextMap.getActionIndex(), i),
(int)(event.getHistoricalX(aTouchContextMap.getActionIndex(), i) + xOffset),
(int)(event.getHistoricalY(aTouchContextMap.getActionIndex(), i) + yOffset),
event.getHistoricalEventTime(i));
}
}
@@ -1662,8 +1682,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
{
aTouchContextMap.touchMoveEvent(
(int)event.getX(aTouchContextMap.getActionIndex()),
(int)event.getY(aTouchContextMap.getActionIndex()),
(int)(event.getX(aTouchContextMap.getActionIndex()) + xOffset),
(int)(event.getY(aTouchContextMap.getActionIndex()) + yOffset),
event.getEventTime());
}
}
@@ -1687,22 +1707,27 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return handleMotionEvent(null, event) || super.onTouchEvent(event);
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
return handleMotionEvent(null, event) || super.onGenericMotionEvent(event);
}
private void updateMousePosition(View view, MotionEvent event) {
private void updateMousePosition(View touchedView, MotionEvent event) {
// X and Y are already relative to the provided view object
float eventX = event.getX(0);
float eventY = event.getY(0);
float eventX, eventY;
// For our StreamView itself, we can use the coordinates unmodified.
if (touchedView == streamView) {
eventX = event.getX(0);
eventY = event.getY(0);
}
else {
// For the containing background view, we must subtract the origin
// of the StreamView to get video-relative coordinates.
eventX = event.getX(0) - streamView.getX();
eventY = event.getY(0) - streamView.getY();
}
if (event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
(event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER ||
@@ -1735,10 +1760,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Normalize these to the view size. We can't just drop them because we won't always get an event
// right at the boundary of the view, so dropping them would result in our cursor never really
// reaching the sides of the screen.
eventX = Math.min(Math.max(eventX, 0), view.getWidth());
eventY = Math.min(Math.max(eventY, 0), view.getHeight());
eventX = Math.min(Math.max(eventX, 0), streamView.getWidth());
eventY = Math.min(Math.max(eventY, 0), streamView.getHeight());
conn.sendMousePosition((short)eventX, (short)eventY, (short)view.getWidth(), (short)view.getHeight());
conn.sendMousePosition((short)eventX, (short)eventY, (short)streamView.getWidth(), (short)streamView.getHeight());
}
@Override
@@ -1757,6 +1782,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
view.requestUnbufferedDispatch(event);
}
}
return handleMotionEvent(view, event);
}
@@ -1887,6 +1913,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
message = getResources().getString(R.string.early_termination_error);
break;
case MoonBridge.ML_ERROR_FRAME_CONVERSION:
message = getResources().getString(R.string.frame_conversion_error);
break;
default:
message = getResources().getString(R.string.conn_terminated_msg);
break;
@@ -2167,4 +2197,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
suppressPipRefCount--;
updatePipAutoEnter();
}
@Override
public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
switch (keyEvent.getAction()) {
case KeyEvent.ACTION_DOWN:
return handleKeyDown(keyEvent);
case KeyEvent.ACTION_UP:
return handleKeyUp(keyEvent);
default:
return false;
}
}
}
@@ -9,6 +9,8 @@ import android.hardware.usb.UsbManager;
import android.media.AudioAttributes;
import android.os.Build;
import android.os.CombinedVibration;
import android.os.Handler;
import android.os.Looper;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
@@ -34,8 +36,6 @@ import com.limelight.utils.Vector2d;
import org.cgutman.shieldcontrollerextensions.SceManager;
import java.lang.reflect.InvocationTargetException;
import java.util.Timer;
import java.util.TimerTask;
public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
@@ -60,6 +60,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final GameGestures gestures;
private final Vibrator deviceVibrator;
private final SceManager sceManager;
private final Handler handler;
private boolean hasGameController;
private final PreferenceConfiguration prefConfig;
@@ -71,6 +72,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
this.gestures = gestures;
this.prefConfig = prefConfig;
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
this.handler = new Handler(Looper.getMainLooper());
this.sceManager = new SceManager(activityContext);
this.sceManager.start();
@@ -1292,28 +1294,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
private void toggleMouseEmulation(final GenericControllerContext context) {
if (context.mouseEmulationTimer != null) {
context.mouseEmulationTimer.cancel();
context.mouseEmulationTimer = null;
}
context.mouseEmulationActive = !context.mouseEmulationActive;
Toast.makeText(activityContext, "Mouse emulation is: " + (context.mouseEmulationActive ? "ON" : "OFF"), Toast.LENGTH_SHORT).show();
if (context.mouseEmulationActive) {
context.mouseEmulationTimer = new Timer();
context.mouseEmulationTimer.schedule(new TimerTask() {
@Override
public void run() {
// Send mouse movement events from analog sticks
sendEmulatedMouseEvent(context.leftStickX, context.leftStickY);
sendEmulatedMouseEvent(context.rightStickX, context.rightStickY);
}
}, 50, 50);
}
}
@TargetApi(31)
private boolean hasDualAmplitudeControlledRumbleVibrators(VibratorManager vm) {
int[] vibratorIds = vm.getVibratorIds();
@@ -1531,7 +1511,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
event.getEventTime() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS &&
prefConfig.mouseEmulation) {
toggleMouseEmulation(context);
context.toggleMouseEmulation();
}
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
@@ -1878,7 +1858,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
usbDeviceContexts.put(controller.getControllerId(), context);
}
static class GenericControllerContext {
class GenericControllerContext {
public int id;
public boolean external;
@@ -1902,14 +1882,38 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public short leftStickY = 0x0000;
public boolean mouseEmulationActive;
public Timer mouseEmulationTimer;
public short mouseEmulationLastInputMap;
public final int mouseEmulationReportPeriod = 50;
public final Runnable mouseEmulationRunnable = new Runnable() {
@Override
public void run() {
if (!mouseEmulationActive) {
return;
}
// Send mouse movement events from analog sticks
sendEmulatedMouseEvent(leftStickX, leftStickY);
sendEmulatedMouseEvent(rightStickX, rightStickY);
// Requeue the callback
handler.postDelayed(this, mouseEmulationReportPeriod);
}
};
public void toggleMouseEmulation() {
handler.removeCallbacks(mouseEmulationRunnable);
mouseEmulationActive = !mouseEmulationActive;
Toast.makeText(activityContext, "Mouse emulation is: " + (mouseEmulationActive ? "ON" : "OFF"), Toast.LENGTH_SHORT).show();
if (mouseEmulationActive) {
handler.postDelayed(mouseEmulationRunnable, mouseEmulationReportPeriod);
}
}
public void destroy() {
if (mouseEmulationTimer != null) {
mouseEmulationTimer.cancel();
mouseEmulationTimer = null;
}
mouseEmulationActive = false;
handler.removeCallbacks(mouseEmulationRunnable);
}
}
@@ -7,9 +7,6 @@ import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class AbsoluteTouchContext implements TouchContext {
private int lastTouchDownX = 0;
private int lastTouchDownY = 0;
@@ -22,8 +19,29 @@ public class AbsoluteTouchContext implements TouchContext {
private boolean cancelled;
private boolean confirmedLongPress;
private boolean confirmedTap;
private Timer longPressTimer;
private Timer tapDownTimer;
private final Runnable longPressRunnable = new Runnable() {
@Override
public void run() {
// This timer should have already expired, but cancel it just in case
cancelTapDownTimer();
// Switch from a left click to a right click after a long press
confirmedLongPress = true;
if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
};
private final Runnable tapDownRunnable = new Runnable() {
@Override
public void run() {
// Start our tap
tapConfirmed();
}
};
private final NvConnection conn;
private final int actionIndex;
@@ -136,67 +154,22 @@ public class AbsoluteTouchContext implements TouchContext {
lastTouchUpTime = eventTime;
}
private synchronized void startLongPressTimer() {
longPressTimer = new Timer(true);
longPressTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (longPressTimer == null) {
return;
}
// Uncancellable now
longPressTimer = null;
// This timer should have already expired, but cancel it just in case
cancelTapDownTimer();
// Switch from a left click to a right click after a long press
confirmedLongPress = true;
if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
}
}, LONG_PRESS_TIME_THRESHOLD);
private void startLongPressTimer() {
cancelLongPressTimer();
handler.postDelayed(longPressRunnable, LONG_PRESS_TIME_THRESHOLD);
}
private synchronized void cancelLongPressTimer() {
if (longPressTimer != null) {
longPressTimer.cancel();
longPressTimer = null;
}
private void cancelLongPressTimer() {
handler.removeCallbacks(longPressRunnable);
}
private synchronized void startTapDownTimer() {
tapDownTimer = new Timer(true);
tapDownTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (tapDownTimer == null) {
return;
}
// Uncancellable now
tapDownTimer = null;
// Start our tap
tapConfirmed();
}
}
}, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
private void startTapDownTimer() {
cancelTapDownTimer();
handler.postDelayed(tapDownRunnable, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
}
private synchronized void cancelTapDownTimer() {
if (tapDownTimer != null) {
tapDownTimer.cancel();
tapDownTimer = null;
}
private void cancelTapDownTimer() {
handler.removeCallbacks(tapDownRunnable);
}
private void tapConfirmed() {
@@ -8,9 +8,6 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.Timer;
import java.util.TimerTask;
public class RelativeTouchContext implements TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
@@ -21,7 +18,6 @@ public class RelativeTouchContext implements TouchContext {
private boolean confirmedMove;
private boolean confirmedDrag;
private boolean confirmedScroll;
private Timer dragTimer;
private double distanceMoved;
private double xFactor, yFactor;
private int pointerCount;
@@ -35,6 +31,25 @@ public class RelativeTouchContext implements TouchContext {
private final PreferenceConfiguration prefConfig;
private final Handler handler;
private final Runnable dragTimerRunnable = new Runnable() {
@Override
public void run() {
// Check if someone already set move
if (confirmedMove) {
return;
}
// The drag should only be processed for the primary finger
if (actionIndex != maxPointerCountInGesture - 1) {
return;
}
// We haven't been cancelled before the timer expired so begin dragging
confirmedDrag = true;
conn.sendMouseButtonDown(getMouseButtonIndex());
}
};
// Indexed by MouseButtonPacket.BUTTON_XXX - 1
private final Runnable[] buttonUpRunnables = new Runnable[] {
new Runnable() {
@@ -184,49 +199,16 @@ public class RelativeTouchContext implements TouchContext {
}
}
private synchronized void startDragTimer() {
// Cancel any existing drag timers
private void startDragTimer() {
cancelDragTimer();
dragTimer = new Timer(true);
dragTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (RelativeTouchContext.this) {
// Check if someone already set move
if (confirmedMove) {
return;
}
// The drag should only be processed for the primary finger
if (actionIndex != maxPointerCountInGesture - 1) {
return;
}
// Check if someone cancelled us
if (dragTimer == null) {
return;
}
// Uncancellable now
dragTimer = null;
// We haven't been cancelled before the timer expired so begin dragging
confirmedDrag = true;
conn.sendMouseButtonDown(getMouseButtonIndex());
}
}
}, DRAG_TIME_THRESHOLD);
handler.postDelayed(dragTimerRunnable, DRAG_TIME_THRESHOLD);
}
private synchronized void cancelDragTimer() {
if (dragTimer != null) {
dragTimer.cancel();
dragTimer = null;
}
private void cancelDragTimer() {
handler.removeCallbacks(dragTimerRunnable);
}
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
private void checkForConfirmedMove(int eventX, int eventY) {
// If we've already confirmed something, get out now
if (confirmedMove || confirmedDrag) {
return;
@@ -305,8 +305,7 @@ public class AnalogStick extends VirtualControllerElement {
// handle event depending on action
switch (event.getActionMasked()) {
// down event (touch event)
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
case MotionEvent.ACTION_DOWN: {
// set to dead zoned, will be corrected in update position if necessary
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
// check for double click
@@ -325,8 +324,8 @@ public class AnalogStick extends VirtualControllerElement {
break;
}
// up event (revoke touch)
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
setPressed(false);
break;
}
@@ -14,8 +14,6 @@ import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
/**
* This is a digital button on screen element. It is used to get click and double click user input.
@@ -43,22 +41,16 @@ public class DigitalButton extends VirtualControllerElement {
void onRelease();
}
/**
*
*/
private class TimerLongClickTimerTask extends TimerTask {
@Override
public void run() {
onLongClickCallback();
}
}
private List<DigitalButtonListener> listeners = new ArrayList<>();
private String text = "";
private int icon = -1;
private long timerLongClickTimeout = 3000;
private Timer timerLongClick = null;
private TimerLongClickTimerTask longClickTimerTask = null;
private final Runnable longClickRunnable = new Runnable() {
@Override
public void run() {
onLongClickCallback();
}
};
private final Paint paint = new Paint();
private final RectF rect = new RectF();
@@ -177,18 +169,8 @@ public class DigitalButton extends VirtualControllerElement {
listener.onClick();
}
if (timerLongClick != null) {
timerLongClick.cancel();
timerLongClick = null;
}
if (longClickTimerTask != null) {
longClickTimerTask.cancel();
longClickTimerTask = null;
}
timerLongClick = new Timer();
longClickTimerTask = new TimerLongClickTimerTask();
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
virtualController.getHandler().removeCallbacks(longClickRunnable);
virtualController.getHandler().postDelayed(longClickRunnable, timerLongClickTimeout);
}
private void onLongClickCallback() {
@@ -207,14 +189,7 @@ public class DigitalButton extends VirtualControllerElement {
}
// We may be called for a release without a prior click
if (timerLongClick != null) {
timerLongClick.cancel();
timerLongClick = null;
}
if (longClickTimerTask != null) {
longClickTimerTask.cancel();
longClickTimerTask = null;
}
virtualController.getHandler().removeCallbacks(longClickRunnable);
}
@Override
@@ -225,8 +200,7 @@ public class DigitalButton extends VirtualControllerElement {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
case MotionEvent.ACTION_DOWN: {
movingButton = null;
setPressed(true);
onClickCallback();
@@ -241,8 +215,7 @@ public class DigitalButton extends VirtualControllerElement {
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
case MotionEvent.ACTION_UP: {
setPressed(false);
onReleaseCallback();
@@ -162,7 +162,6 @@ public class DigitalPad extends VirtualControllerElement {
// get masked (not specific to a pointer) action
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_MOVE: {
direction = 0;
@@ -184,8 +183,7 @@ public class DigitalPad extends VirtualControllerElement {
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
case MotionEvent.ACTION_UP: {
direction = 0;
newDirectionCallback(direction);
invalidate();
@@ -5,6 +5,8 @@
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.Button;
@@ -17,8 +19,6 @@ import com.limelight.binding.input.ControllerHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
public class VirtualController {
public static class ControllerInputContext {
@@ -41,11 +41,17 @@ public class VirtualController {
private final ControllerHandler controllerHandler;
private final Context context;
private final Handler handler;
private final Runnable delayedRetransmitRunnable = new Runnable() {
@Override
public void run() {
sendControllerInputContextInternal();
}
};
private FrameLayout frame_layout = null;
private Timer retransmitTimer;
ControllerMode currentMode = ControllerMode.Active;
ControllerInputContext inputContext = new ControllerInputContext();
@@ -57,6 +63,7 @@ public class VirtualController {
this.controllerHandler = controllerHandler;
this.frame_layout = layout;
this.context = context;
this.handler = new Handler(Looper.getMainLooper());
buttonConfigure = new Button(context);
buttonConfigure.setAlpha(0.25f);
@@ -91,9 +98,11 @@ public class VirtualController {
}
public void hide() {
retransmitTimer.cancel();
Handler getHandler() {
return handler;
}
public void hide() {
for (VirtualControllerElement element : elements) {
element.setVisibility(View.INVISIBLE);
}
@@ -107,18 +116,6 @@ public class VirtualController {
}
buttonConfigure.setVisibility(View.VISIBLE);
// HACK: GFE sometimes discards gamepad packets when they are received
// very shortly after another. This can be critical if an axis zeroing packet
// is lost and causes an analog stick to get stuck. To avoid this, we send
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
retransmitTimer = new Timer("OSC timer", true);
retransmitTimer.schedule(new TimerTask() {
@Override
public void run() {
sendControllerInputContext();
}
}, 100, 100);
}
public void removeElements() {
@@ -181,7 +178,7 @@ public class VirtualController {
return inputContext;
}
void sendControllerInputContext() {
private void sendControllerInputContextInternal() {
_DBG("INPUT_MAP + " + inputContext.inputMap);
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
@@ -200,4 +197,19 @@ public class VirtualController {
);
}
}
void sendControllerInputContext() {
// Cancel retransmissions of prior gamepad inputs
handler.removeCallbacks(delayedRetransmitRunnable);
sendControllerInputContextInternal();
// HACK: GFE sometimes discards gamepad packets when they are received
// very shortly after another. This can be critical if an axis zeroing packet
// is lost and causes an analog stick to get stuck. To avoid this, we retransmit
// the gamepad state a few times unless another input event happens before then.
handler.postDelayed(delayedRetransmitRunnable, 25);
handler.postDelayed(delayedRetransmitRunnable, 50);
handler.postDelayed(delayedRetransmitRunnable, 75);
}
}
@@ -40,27 +40,31 @@ public class VirtualControllerConfigurationLoader {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
controller.sendControllerInputContext();
return;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) != 0) {
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
else {
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) != 0) {
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
else {
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) != 0) {
inputContext.inputMap |= ControllerPacket.UP_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
else {
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) != 0) {
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
}
else {
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
}
controller.sendControllerInputContext();
}
});
@@ -223,13 +223,21 @@ public abstract class VirtualControllerElement extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
// Ignore secondary touches on controls
//
// NB: We can get an additional pointer down if the user touches a non-StreamView area
// while also touching an OSC control, even if that pointer down doesn't correspond to
// an area of the OSC control.
if (event.getActionIndex() != 0) {
return true;
}
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
return onElementTouchEvent(event);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
case MotionEvent.ACTION_DOWN: {
position_pressed_x = event.getX();
position_pressed_y = event.getY();
startSize_x = getWidth();
@@ -267,8 +275,7 @@ public abstract class VirtualControllerElement extends View {
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
case MotionEvent.ACTION_UP: {
actionCancel();
return true;
}
@@ -1,14 +1,16 @@
package com.limelight.binding.video;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import org.jcodec.codecs.h264.H264Utils;
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
import org.jcodec.codecs.h264.io.model.VUIParameters;
import com.limelight.BuildConfig;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
@@ -71,6 +73,23 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
private boolean foreground = true;
private PerfOverlayListener perfListener;
private static final int CR_TIMEOUT_MS = 5000;
private static final int CR_MAX_TRIES = 10;
private static final int CR_RECOVERY_TYPE_NONE = 0;
private static final int CR_RECOVERY_TYPE_RESTART = 1;
private static final int CR_RECOVERY_TYPE_RESET = 2;
private AtomicInteger codecRecoveryType = new AtomicInteger(CR_RECOVERY_TYPE_NONE);
private final Object codecRecoveryMonitor = new Object();
// Each thread that touches the MediaCodec object or any associated buffers must have a flag
// here and must call doCodecRecoveryIfRequired() on a regular basis.
private static final int CR_FLAG_INPUT_THREAD = 0x1;
private static final int CR_FLAG_RENDER_THREAD = 0x2;
private static final int CR_FLAG_CHOREOGRAPHER = 0x4;
private static final int CR_FLAG_ALL = CR_FLAG_INPUT_THREAD | CR_FLAG_RENDER_THREAD | CR_FLAG_CHOREOGRAPHER;
private int codecRecoveryThreadQuiescedFlags = 0;
private int codecRecoveryAttempts = 0;
private MediaFormat inputFormat;
private MediaFormat outputFormat;
private MediaFormat configuredFormat;
@@ -179,7 +198,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
// for even required levels of HEVC.
MediaCodecInfo hevcDecoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
if (hevcDecoderInfo != null) {
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(hevcDecoderInfo.getName(), meteredNetwork, prefs)) {
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(hevcDecoderInfo.getName())) {
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+hevcDecoderInfo.getName());
// Force HEVC enabled if the user asked for it
@@ -337,68 +356,68 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
return videoFormat;
}
private boolean tryConfigureDecoder(MediaCodecInfo selectedDecoderInfo, MediaFormat format) {
try {
videoDecoder = MediaCodec.createByCodecName(selectedDecoderInfo.getName());
LimeLog.info("Configuring with format: "+format);
private void configureAndStartDecoder(MediaFormat format) {
LimeLog.info("Configuring with format: "+format);
videoDecoder.configure(format, renderTarget.getSurface(), null, 0);
videoDecoder.configure(format, renderTarget.getSurface(), null, 0);
configuredFormat = format;
configuredFormat = format;
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);
}
// After reconfiguration, we must resubmit CSD buffers
submittedCsd = false;
submitCsdNextCall = false;
vpsBuffer = null;
spsBuffer = null;
ppsBuffer = null;
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
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);
}
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
videoDecoder.setOnFrameRenderedListener(new MediaCodec.OnFrameRenderedListener() {
@Override
public void onFrameRendered(MediaCodec mediaCodec, long presentationTimeUs, long renderTimeNanos) {
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
if (USE_FRAME_RENDER_TIME) {
activeWindowVideoStats.totalTimeMs += delta;
}
}
}
}, null);
}
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
LimeLog.info("Using codec "+selectedDecoderInfo.getName()+" for hardware decoding "+format.getString(MediaFormat.KEY_MIME));
// Start the decoder
videoDecoder.start();
// Start the decoder
videoDecoder.start();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
legacyInputBuffers = videoDecoder.getInputBuffers();
}
fetchNextInputBuffer();
return true;
} catch (Exception e) {
e.printStackTrace();
if (videoDecoder != null) {
videoDecoder.release();
videoDecoder = null;
}
return false;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
legacyInputBuffers = videoDecoder.getInputBuffers();
}
}
@Override
public int setup(int format, int width, int height, int redrawRate) {
this.initialWidth = width;
this.initialHeight = height;
this.videoFormat = format;
this.refreshRate = redrawRate;
private boolean tryConfigureDecoder(MediaCodecInfo selectedDecoderInfo, MediaFormat format, boolean throwOnCodecError) {
boolean configured = false;
try {
videoDecoder = MediaCodec.createByCodecName(selectedDecoderInfo.getName());
configureAndStartDecoder(format);
LimeLog.info("Using codec " + selectedDecoderInfo.getName() + " for hardware decoding " + format.getString(MediaFormat.KEY_MIME));
configured = true;
} catch (IllegalArgumentException e) {
e.printStackTrace();
if (throwOnCodecError) {
throw e;
}
} catch (IllegalStateException e) {
e.printStackTrace();
if (throwOnCodecError) {
throw e;
}
} catch (IOException e) {
e.printStackTrace();
if (throwOnCodecError) {
throw new RuntimeException(e);
}
} finally {
if (!configured && videoDecoder != null) {
videoDecoder.release();
videoDecoder = null;
}
}
return configured;
}
public int initializeDecoder(boolean throwOnCodecError) {
String mimeType;
MediaCodecInfo selectedDecoderInfo;
@@ -411,7 +430,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
return -1;
}
if (width > 4096 || height > 4096) {
if (initialWidth > 4096 || initialHeight > 4096) {
LimeLog.severe("> 4K streaming only supported on HEVC");
return -1;
}
@@ -464,7 +483,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
// This will try low latency options until we find one that works (or we give up).
boolean newFormat = MediaCodecHelper.setDecoderLowLatencyOptions(mediaFormat, selectedDecoderInfo, tryNumber);
if (tryConfigureDecoder(selectedDecoderInfo, mediaFormat)) {
// Throw the underlying codec exception on the last attempt if the caller requested it
if (tryConfigureDecoder(selectedDecoderInfo, mediaFormat, !newFormat && throwOnCodecError)) {
// Success!
break;
}
@@ -475,26 +495,233 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
}
}
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
videoDecoder.setOnFrameRenderedListener(new MediaCodec.OnFrameRenderedListener() {
@Override
public void onFrameRendered(MediaCodec mediaCodec, long presentationTimeUs, long renderTimeNanos) {
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
if (USE_FRAME_RENDER_TIME) {
activeWindowVideoStats.totalTimeMs += delta;
}
}
}
}, null);
}
return 0;
}
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags, boolean throwOnTransient) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
@Override
public int setup(int format, int width, int height, int redrawRate) {
this.initialWidth = width;
this.initialHeight = height;
this.videoFormat = format;
this.refreshRate = redrawRate;
if (codecExc.isTransient() && !throwOnTransient) {
// We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo());
return;
return initializeDecoder(false);
}
// All threads that interact with the MediaCodec instance must call this function regularly!
private boolean doCodecRecoveryIfRequired(int quiescenceFlag) {
// NB: We cannot check 'stopping' here because we could end up bailing in a partially
// quiesced state that will cause the quiesced threads to never wake up.
if (codecRecoveryType.get() == CR_RECOVERY_TYPE_NONE) {
// Common case
return false;
}
// We need some sort of recovery, so quiesce all threads before starting that
synchronized (codecRecoveryMonitor) {
if (choreographerHandlerThread == null) {
// If we have no choreographer thread, we can just mark that as quiesced right now.
codecRecoveryThreadQuiescedFlags |= CR_FLAG_CHOREOGRAPHER;
}
codecRecoveryThreadQuiescedFlags |= quiescenceFlag;
if (codecRecoveryThreadQuiescedFlags == CR_FLAG_ALL) {
// This is the final thread to quiesce, so let's perform the codec recovery now.
codecRecoveryAttempts++;
LimeLog.info("Codec recovery attempt: "+codecRecoveryAttempts);
// Input and output buffers are invalidated by stop() and reset().
nextInputBuffer = null;
nextInputBufferIndex = -1;
outputBufferQueue.clear();
// For "recoverable" exceptions, we can just stop, reconfigure, and restart.
if (codecRecoveryType.get() == CR_RECOVERY_TYPE_RESTART) {
LimeLog.warning("Trying to restart decoder after CodecException");
try {
videoDecoder.stop();
configureAndStartDecoder(configuredFormat);
codecRecoveryType.set(CR_RECOVERY_TYPE_NONE);
} catch (IllegalArgumentException e) {
e.printStackTrace();
// Our Surface is probably invalid, so just stop
stopping = true;
codecRecoveryType.set(CR_RECOVERY_TYPE_NONE);
} catch (IllegalStateException e) {
e.printStackTrace();
// Something went wrong during the restart, let's use a bigger hammer
// and try a reset instead.
codecRecoveryType.set(CR_RECOVERY_TYPE_RESET);
}
}
LimeLog.severe(codecExc.getDiagnosticInfo());
// For "non-recoverable" exceptions on L+, we can call reset() to recover
// without having to recreate the entire decoder again.
if (codecRecoveryType.get() == CR_RECOVERY_TYPE_RESET && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
LimeLog.warning("Trying to reset decoder after CodecException");
try {
videoDecoder.reset();
configureAndStartDecoder(configuredFormat);
codecRecoveryType.set(CR_RECOVERY_TYPE_NONE);
} catch (IllegalArgumentException e) {
e.printStackTrace();
// Our Surface is probably invalid, so just stop
stopping = true;
codecRecoveryType.set(CR_RECOVERY_TYPE_NONE);
} catch (IllegalStateException e) {
e.printStackTrace();
// Something went wrong during the reset, we'll have to resort to
// releasing and recreating the decoder now.
}
}
// If we _still_ haven't managed to recover, go for the nuclear option and just
// throw away the old decoder and reinitialize a new one from scratch.
if (codecRecoveryType.get() == CR_RECOVERY_TYPE_RESET) {
LimeLog.warning("Trying to recreate decoder after CodecException");
videoDecoder.release();
try {
int err = initializeDecoder(true);
if (err != 0) {
throw new IllegalStateException("Decoder reset failed: " + err);
}
codecRecoveryType.set(CR_RECOVERY_TYPE_NONE);
} catch (IllegalArgumentException e) {
e.printStackTrace();
// Our Surface is probably invalid, so just stop
stopping = true;
codecRecoveryType.set(CR_RECOVERY_TYPE_NONE);
} catch (IllegalStateException e) {
// If we failed to recover after all of these attempts, just crash
if (!reportedCrash) {
reportedCrash = true;
crashListener.notifyCrash(e);
}
throw new RendererException(this, e);
}
}
// Wake all quiesced threads and allow them to begin work again
codecRecoveryThreadQuiescedFlags = 0;
codecRecoveryMonitor.notifyAll();
}
else {
// If we haven't quiesced all threads yet, wait to be signalled after recovery.
// The final thread to be quiesced will handle the codec recovery.
LimeLog.info("Waiting to quiesce decoder threads: "+codecRecoveryThreadQuiescedFlags);
long startTime = SystemClock.uptimeMillis();
while (codecRecoveryType.get() != CR_RECOVERY_TYPE_NONE) {
try {
if (SystemClock.uptimeMillis() - startTime >= CR_TIMEOUT_MS) {
throw new IllegalStateException("Decoder failed to recover within timeout");
}
codecRecoveryMonitor.wait(CR_TIMEOUT_MS);
} catch (InterruptedException e) {
e.printStackTrace();
// InterruptedException clears the thread's interrupt status. Since we can't
// handle that here, we will re-interrupt the thread to set the interrupt
// status back to true.
Thread.currentThread().interrupt();
break;
}
}
}
}
// Only throw if we're not stopping
if (!stopping) {
return true;
}
// Returns true if the exception is transient
private boolean handleDecoderException(IllegalStateException e) {
// Eat decoder exceptions if we're in the process of stopping
if (stopping) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
if (codecExc.isTransient()) {
// We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo());
return true;
}
LimeLog.severe(codecExc.getDiagnosticInfo());
// We can attempt a recovery or reset at this stage to try to start decoding again
if (codecRecoveryAttempts < CR_MAX_TRIES) {
// If the exception is non-recoverable or we already require a reset, perform a reset.
// If we have no prior unrecoverable failure, we will try a restart instead.
if (codecExc.isRecoverable() && codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_NONE, CR_RECOVERY_TYPE_RESTART)) {
LimeLog.info("Decoder requires restart for recoverable CodecException");
e.printStackTrace();
}
else if (!codecExc.isRecoverable()) {
if (codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_NONE, CR_RECOVERY_TYPE_RESET)) {
LimeLog.info("Decoder requires reset for non-recoverable CodecException");
e.printStackTrace();
}
else if (codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_RESTART, CR_RECOVERY_TYPE_RESET)) {
LimeLog.info("Decoder restart promoted to reset for non-recoverable CodecException");
e.printStackTrace();
}
else if (codecRecoveryType.get() != CR_RECOVERY_TYPE_RESET) {
throw new IllegalStateException("Unexpected codec recovery type" + codecRecoveryType.get());
}
}
// The recovery will take place when all threads reach doCodecRecoveryIfRequired().
return false;
}
}
else {
// IllegalStateException was primarily used prior to the introduction of CodecException.
// Recovery from this requires a full decoder reset.
//
// NB: CodecException is an IllegalStateException, so we must check for it first.
if (codecRecoveryAttempts < CR_MAX_TRIES) {
if (codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_NONE, CR_RECOVERY_TYPE_RESET)) {
LimeLog.info("Decoder requires reset for IllegalStateException");
e.printStackTrace();
}
else if (codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_RESTART, CR_RECOVERY_TYPE_RESET)) {
LimeLog.info("Decoder restart promoted to reset for IllegalStateException");
e.printStackTrace();
}
else if (codecRecoveryType.get() != CR_RECOVERY_TYPE_RESET) {
throw new IllegalStateException("Unexpected codec recovery type: " + codecRecoveryType.get());
}
return false;
}
}
// Only throw if we're not in the middle of codec recovery
if (codecRecoveryType.get() == CR_RECOVERY_TYPE_NONE) {
//
// There seems to be a race condition with decoder/surface teardown causing some
// decoders to to throw IllegalStateExceptions even before 'stopping' is set.
@@ -515,15 +742,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
}
else {
// This is the first exception we've hit
if (buf != null || codecFlags != 0) {
initialException = new RendererException(this, e, buf, codecFlags);
}
else {
initialException = new RendererException(this, e);
}
initialException = new RendererException(this, e);
initialExceptionTimestamp = SystemClock.uptimeMillis();
}
}
// Not transient
return false;
}
@Override
@@ -555,13 +780,23 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
lastRenderedFrameTimeNanos = frameTimeNanos;
activeWindowVideoStats.totalFramesRendered++;
} catch (Exception e) {
// This will leak nextOutputBuffer, but there's really nothing else we can do
handleDecoderException(e, null, 0, false);
} catch (IllegalStateException ignored) {
try {
// Try to avoid leaking the output buffer by releasing it without rendering
videoDecoder.releaseOutputBuffer(nextOutputBuffer, false);
} catch (IllegalStateException e) {
// This will leak nextOutputBuffer, but there's really nothing else we can do
e.printStackTrace();
handleDecoderException(e);
}
}
}
}
// Attempt codec recovery even if we have nothing to render right now. Recovery can still
// be required even if the codec died before giving any output.
doCodecRecoveryIfRequired(CR_FLAG_CHOREOGRAPHER);
// Request another callback for next frame
Choreographer.getInstance().postFrameCallback(this);
}
@@ -648,7 +883,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
// run for a while (if there is a huge mismatch between stream FPS and display
// refresh rate).
if (outputBufferQueue.size() == OUTPUT_BUFFER_QUEUE_LIMIT) {
videoDecoder.releaseOutputBuffer(outputBufferQueue.take(), false);
try {
videoDecoder.releaseOutputBuffer(outputBufferQueue.take(), false);
} catch (InterruptedException e) {
// We're shutting down, so we can just drop this buffer on the floor
// and it will be reclaimed when the codec is released.
return;
}
}
// Add this buffer
@@ -676,8 +917,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
break;
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0, false);
} catch (IllegalStateException e) {
handleDecoderException(e);
} finally {
doCodecRecoveryIfRequired(CR_FLAG_RENDER_THREAD);
}
}
}
@@ -689,8 +932,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
private boolean fetchNextInputBuffer() {
long startTime;
boolean codecRecovered;
if (nextInputBufferIndex >= 0) {
if (nextInputBuffer != null) {
// We already have an input buffer
return true;
}
@@ -698,15 +942,23 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
startTime = SystemClock.uptimeMillis();
try {
// If we don't have an input buffer index yet, fetch one now
while (nextInputBufferIndex < 0 && !stopping) {
nextInputBufferIndex = videoDecoder.dequeueInputBuffer(10000);
}
// Get the backing ByteBuffer for the input buffer index
if (nextInputBufferIndex >= 0) {
// Using the new getInputBuffer() API on Lollipop allows
// the framework to do some performance optimizations for us
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
nextInputBuffer = videoDecoder.getInputBuffer(nextInputBufferIndex);
if (nextInputBuffer == null) {
// According to the Android docs, getInputBuffer() can return null "if the
// index is not a dequeued input buffer". I don't think this ever should
// happen but if it does, let's try to get a new input buffer next time.
nextInputBufferIndex = -1;
}
}
else {
nextInputBuffer = legacyInputBuffers[nextInputBufferIndex];
@@ -715,8 +967,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
nextInputBuffer.clear();
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0, true);
} catch (IllegalStateException e) {
handleDecoderException(e);
return false;
} finally {
codecRecovered = doCodecRecoveryIfRequired(CR_FLAG_INPUT_THREAD);
}
// If codec recovery is required, always return false to ensure the caller will request
// an IDR frame to complete the codec recovery.
if (codecRecovered) {
return false;
}
@@ -726,7 +986,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
LimeLog.warning("Dequeue input buffer ran long: " + deltaMs + " ms");
}
if (nextInputBufferIndex < 0) {
if (nextInputBuffer == null) {
// We've been hung for 5 seconds and no other exception was reported,
// so generate a decoder hung exception
if (deltaMs >= 5000 && initialException == null) {
@@ -760,6 +1020,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
rendererThread.interrupt();
}
// Stop any active codec recovery operations
synchronized (codecRecoveryMonitor) {
codecRecoveryType.set(CR_RECOVERY_TYPE_NONE);
codecRecoveryMonitor.notifyAll();
}
// Post a quit message to the Choreographer looper (if we have one)
if (choreographerHandler != null) {
choreographerHandler.post(new Runnable() {
@@ -818,23 +1084,47 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
}
private boolean queueNextInputBuffer(long timestampUs, int codecFlags) {
boolean codecRecovered;
try {
videoDecoder.queueInputBuffer(nextInputBufferIndex,
0, nextInputBuffer.position(),
timestampUs, codecFlags);
} catch (Exception e) {
handleDecoderException(e, null, codecFlags, true);
return false;
} finally {
// We need a new buffer now
nextInputBufferIndex = -1;
nextInputBuffer = null;
} catch (IllegalStateException e) {
if (handleDecoderException(e)) {
// We encountered a transient error. In this case, just hold onto the buffer
// (to avoid leaking it), clear it, and keep it for the next frame. We'll return
// false to trigger an IDR frame to recover.
nextInputBuffer.clear();
}
else {
// We encountered a non-transient error. In this case, we will simply leak the
// buffer because we cannot be sure we will ever succeed in queuing it.
nextInputBufferIndex = -1;
nextInputBuffer = null;
}
return false;
} finally {
codecRecovered = doCodecRecoveryIfRequired(CR_FLAG_INPUT_THREAD);
}
// If codec recovery is required, always return false to ensure the caller will request
// an IDR frame to complete the codec recovery.
if (codecRecovered) {
return false;
}
// Fetch a new input buffer now while we have some time between frames
// to have it ready immediately when the next frame arrives.
fetchNextInputBuffer();
return true;
//
// We must propagate the return value here in order to properly handle
// codec recovery happening in fetchNextInputBuffer(). If we don't, we'll
// never get an IDR frame to complete the recovery process.
return fetchNextInputBuffer();
}
private void doProfileSpecificSpsPatching(SeqParameterSet sps) {
@@ -971,7 +1261,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
// GFE 2.5.11 changed the SPS to add additional extensions
// Some devices don't like these so we remove them here on old devices.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && sps.vuiParams != null) {
sps.vuiParams.videoSignalTypePresentFlag = false;
sps.vuiParams.colourDescriptionPresentFlag = false;
sps.vuiParams.chromaLocInfoPresentFlag = false;
@@ -983,11 +1273,19 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
// or max_dec_frame_buffering which increases decoding latency on Tegra.
// If the encoder didn't include VUI parameters in the SPS, add them now
if (sps.vuiParams == null) {
LimeLog.info("Adding VUI parameters");
sps.vuiParams = new VUIParameters();
}
// GFE 2.5.11 started sending bitstream restrictions
if (sps.vuiParams.bitstreamRestriction == null) {
LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.motionVectorsOverPicBoundariesFlag = true;
sps.vuiParams.bitstreamRestriction.maxBytesPerPicDenom = 2;
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthHorizontal = 16;
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthVertical = 16;
sps.vuiParams.bitstreamRestriction.numReorderFrames = 0;
@@ -1001,13 +1299,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
// These values are the defaults for the fields, but they are more aggressive
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
sps.vuiParams.bitstreamRestriction.maxBytesPerPicDenom = 2;
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
// We'll leave these alone for "modern" devices just in case they care.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
sps.vuiParams.bitstreamRestriction.maxBytesPerPicDenom = 2;
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
}
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
// conservative values by GFE 2.5.11. We'll let those values stand.
}
else {
else if (sps.vuiParams != null) {
// Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11
// will continue to not receive them now
sps.vuiParams.bitstreamRestriction = null;
@@ -1238,7 +1539,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
public String toString() {
String str = "";
str += "Hang time: "+hangTimeMs+" ms\n";
str += "Hang time: "+hangTimeMs+" ms"+ RendererException.DELIMITER;
str += super.toString();
return str;
@@ -1247,22 +1548,19 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
static class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
protected static final String DELIMITER = BuildConfig.DEBUG ? "\n" : " | ";
private String text;
RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.text = generateText(renderer, e, null, 0);
}
RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.text = generateText(renderer, e, currentBuffer, currentCodecFlags);
this.text = generateText(renderer, e);
}
public String toString() {
return text;
}
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) {
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException) {
String str;
if (renderer.numVpsIn == 0 && renderer.numSpsIn == 0 && renderer.numPpsIn == 0) {
@@ -1287,44 +1585,43 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
str = "ErrorWhileStreaming";
}
str += ": 1\n";
str += "Format: "+String.format("%x", renderer.videoFormat)+"\n";
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
str += "Format: "+String.format("%x", renderer.videoFormat)+DELIMITER;
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+DELIMITER;
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+DELIMITER;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
str += "AVC supported width range: "+avcWidthRange+"\n";
str += "AVC supported width range: "+avcWidthRange+DELIMITER;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
Range<Double> avcFpsRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
str += "AVC achievable FPS range: "+avcFpsRange+"\n";
str += "AVC achievable FPS range: "+avcFpsRange+DELIMITER;
} catch (IllegalArgumentException e) {
str += "AVC achievable FPS range: UNSUPPORTED!\n";
str += "AVC achievable FPS range: UNSUPPORTED!"+DELIMITER;
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
str += "HEVC supported width range: "+hevcWidthRange+"\n";
str += "HEVC supported width range: "+hevcWidthRange+DELIMITER;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
Range<Double> hevcFpsRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
str += "HEVC achievable FPS range: " + hevcFpsRange + "\n";
str += "HEVC achievable FPS range: " + hevcFpsRange + DELIMITER;
} catch (IllegalArgumentException e) {
str += "HEVC achievable FPS range: UNSUPPORTED!\n";
str += "HEVC achievable FPS range: UNSUPPORTED!"+DELIMITER;
}
}
}
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 += "GL Renderer: "+renderer.glRenderer+"\n";
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
str += "Configured format: "+renderer.configuredFormat+DELIMITER;
str += "Input format: "+renderer.inputFormat+DELIMITER;
str += "Output format: "+renderer.outputFormat+DELIMITER;
str += "Adaptive playback: "+renderer.adaptivePlayback+DELIMITER;
str += "GL Renderer: "+renderer.glRenderer+DELIMITER;
//str += "Build fingerprint: "+Build.FINGERPRINT+DELIMITER;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
str += "SOC: "+Build.SOC_MANUFACTURER+" - "+Build.SOC_MODEL+"\n";
str += "Performance class: "+Build.VERSION.MEDIA_PERFORMANCE_CLASS+"\n";
str += "Vendor params: ";
str += "SOC: "+Build.SOC_MANUFACTURER+" - "+Build.SOC_MODEL+DELIMITER;
str += "Performance class: "+Build.VERSION.MEDIA_PERFORMANCE_CLASS+DELIMITER;
/*str += "Vendor params: ";
List<String> params = renderer.videoDecoder.getSupportedVendorParameters();
if (params.isEmpty()) {
str += "NONE";
@@ -1334,65 +1631,38 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
str += param + " ";
}
}
str += "\n";
str += DELIMITER;*/
}
str += "Foreground: "+renderer.foreground+"\n";
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n";
str += "Fused IDR frames: "+renderer.fusedIdrFrame+"\n";
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
str += "CSD stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
str += "Frames in-out: "+renderer.numFramesIn+", "+renderer.numFramesOut+"\n";
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
str += "Frame pacing mode: "+renderer.prefs.framePacing+"\n";
if (currentBuffer != null) {
str += "Current buffer: ";
currentBuffer.flip();
while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) {
str += String.format((Locale)null, "%02x ", currentBuffer.get());
}
str += "\n";
str += "Buffer codec flags: "+currentCodecFlags+"\n";
}
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+DELIMITER;
str += "RFI active: "+renderer.refFrameInvalidationActive+DELIMITER;
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+DELIMITER;
str += "Fused IDR frames: "+renderer.fusedIdrFrame+DELIMITER;
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+DELIMITER;
str += "FPS target: "+renderer.refreshRate+DELIMITER;
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps"+DELIMITER;
str += "CSD stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+DELIMITER;
str += "Frames in-out: "+renderer.numFramesIn+", "+renderer.numFramesOut+DELIMITER;
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+DELIMITER;
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+DELIMITER;
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events"+DELIMITER;
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms"+DELIMITER;
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms"+DELIMITER;
str += "Frame pacing mode: "+renderer.prefs.framePacing+DELIMITER;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (originalException instanceof CodecException) {
CodecException ce = (CodecException) originalException;
str += "Diagnostic Info: "+ce.getDiagnosticInfo()+"\n";
str += "Recoverable: "+ce.isRecoverable()+"\n";
str += "Transient: "+ce.isTransient()+"\n";
str += "Diagnostic Info: "+ce.getDiagnosticInfo()+DELIMITER;
str += "Recoverable: "+ce.isRecoverable()+DELIMITER;
str += "Transient: "+ce.isTransient()+DELIMITER;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
str += "Codec Error Code: "+ce.getErrorCode()+"\n";
str += "Codec Error Code: "+ce.getErrorCode()+DELIMITER;
}
}
}
str += "/proc/cpuinfo:\n";
try {
str += MediaCodecHelper.readCpuinfo();
} catch (Exception e) {
str += e.getMessage();
}
str += "Full decoder dump:\n";
try {
str += MediaCodecHelper.dumpDecoders();
} catch (Exception e) {
str += e.getMessage();
}
str += originalException.toString();
return str;
@@ -31,7 +31,6 @@ public class MediaCodecHelper {
private static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
private static final List<String> deprioritizedHevcDecoders;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
private static final List<String> constrainedHighProfilePrefixes;
@@ -44,7 +43,8 @@ public class MediaCodecHelper {
private static final List<String> exynosDecoderPrefixes;
private static final List<String> amlogicDecoderPrefixes;
public static final boolean IS_EMULATOR = Build.HARDWARE.equals("ranchu") || Build.HARDWARE.equals("cheets");
public static final boolean SHOULD_BYPASS_SOFTWARE_BLOCK =
Build.HARDWARE.equals("ranchu") || Build.HARDWARE.equals("cheets") || Build.BRAND.equals("Android-x86");
private static boolean isLowEndSnapdragon = false;
private static boolean isAdreno620 = false;
@@ -70,7 +70,10 @@ public class MediaCodecHelper {
static {
refFrameInvalidationAvcPrefixes = new LinkedList<>();
refFrameInvalidationHevcPrefixes = new LinkedList<>();
refFrameInvalidationHevcPrefixes.add("omx.exynos");
refFrameInvalidationHevcPrefixes.add("c2.exynos");
// Qualcomm and NVIDIA may be added at runtime
}
@@ -82,18 +85,18 @@ public class MediaCodecHelper {
static {
blacklistedDecoderPrefixes = new LinkedList<>();
// Blacklist software decoders that don't support H264 high profile,
// but exclude the official AOSP and CrOS emulator from this restriction.
if (!IS_EMULATOR) {
// Blacklist software decoders that don't support H264 high profile except on systems
// that are expected to only have software decoders (like emulators).
if (!SHOULD_BYPASS_SOFTWARE_BLOCK) {
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
// We want to avoid ffmpeg decoders since they're software decoders,
// but on Android-x86 they might be all we have (and also relatively
// performant on a modern x86 processor).
if (!Build.BRAND.equals("Android-x86")) {
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
// We want to avoid ffmpeg decoders since they're usually software decoders,
// but we'll defer to the Android 10 isSoftwareOnly() API on newer devices
// to determine if we should use these or not.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
}
}
// Force these decoders disabled because:
@@ -194,14 +197,6 @@ public class MediaCodecHelper {
// during initialization to avoid SoCs with broken HEVC decoders.
}
static {
deprioritizedHevcDecoders = new LinkedList<>();
// These are decoders that work but aren't used by default for various reasons.
// Qualcomm is currently the only decoders in this group.
}
static {
useFourSlicesPrefixes = new LinkedList<>();
@@ -324,19 +319,15 @@ public class MediaCodecHelper {
// Tegra K1 and later can do reference frame invalidation properly
if (configInfo.reqGlEsVersion >= 0x30000) {
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
LimeLog.info("Added omx.nvidia to reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
refFrameInvalidationHevcPrefixes.add("omx.nvidia");
LimeLog.info("Added omx.qcom/c2.qti to AVC reference frame invalidation support list");
LimeLog.info("Added omx.qcom/c2.qti to reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.qcom");
refFrameInvalidationHevcPrefixes.add("omx.qcom");
refFrameInvalidationAvcPrefixes.add("c2.qti");
// Prior to M, we were tricking the decoder into using baseline profile, which
// won't support RFI properly.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.intel");
}
refFrameInvalidationHevcPrefixes.add("c2.qti");
}
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
@@ -348,13 +339,9 @@ public class MediaCodecHelper {
// (see comment on isGLES31SnapdragonRenderer).
//
if (isGLES31SnapdragonRenderer(glRenderer)) {
// We prefer reference frame invalidation support (which is only doable on AVC on
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
// HEVC anyway.
LimeLog.info("Added omx.qcom/c2.qti to deprioritized HEVC decoders based on GLES 3.1+ support");
deprioritizedHevcDecoders.add("omx.qcom");
deprioritizedHevcDecoders.add("c2.qti");
LimeLog.info("Added omx.qcom/c2.qti to HEVC decoders based on GLES 3.1+ support");
whitelistedHevcDecoders.add("omx.qcom");
whitelistedHevcDecoders.add("c2.qti");
}
else {
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
@@ -615,7 +602,7 @@ public class MediaCodecHelper {
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
}
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData, PreferenceConfiguration prefs) {
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
// Google didn't have official support for HEVC (or more importantly, a CTS test) until
// Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC,
// so I'm restricting HEVC usage to Lollipop and higher.
@@ -633,21 +620,6 @@ public class MediaCodecHelper {
return false;
}
// Some devices have HEVC decoders that we prefer not to use
// typically because it can't support reference frame invalidation.
// However, we will use it for HDR and for streaming over mobile networks
// since it works fine otherwise. We will also use it for 4K because RFI
// is currently disabled due to issues with video corruption.
if (isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
if (meteredData || (prefs.width == 3840 && prefs.height == 2160)) {
LimeLog.info("Selected deprioritized decoder");
return true;
}
else {
return false;
}
}
return isDecoderInList(whitelistedHevcDecoders, decoderName);
}
@@ -721,7 +693,7 @@ public class MediaCodecHelper {
private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) {
// Use the new isSoftwareOnly() function on Android Q
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (!IS_EMULATOR && codecInfo.isSoftwareOnly()) {
if (!SHOULD_BYPASS_SOFTWARE_BLOCK && codecInfo.isSoftwareOnly()) {
LimeLog.info("Skipping software-only decoder: "+codecInfo.getName());
return true;
}
@@ -45,6 +45,7 @@ public class MoonBridge {
public static final int ML_ERROR_NO_VIDEO_FRAME = -101;
public static final int ML_ERROR_UNEXPECTED_EARLY_TERMINATION = -102;
public static final int ML_ERROR_PROTECTED_CONTENT = -103;
public static final int ML_ERROR_FRAME_CONVERSION = -104;
public static final int ML_PORT_INDEX_TCP_47984 = 0;
public static final int ML_PORT_INDEX_TCP_47989 = 1;
+13 -1
View File
@@ -4,11 +4,23 @@
android:layout_height="match_parent"
tools:context=".Game" >
<View
android:id="@+id/backgroundTouchView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
<com.limelight.ui.StreamView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
android:layout_gravity="center"
android:focusable="true"
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:defaultFocusHighlightEnabled="false">
<requestFocus />
</com.limelight.ui.StreamView>
<TextView
android:id="@+id/performanceOverlay"
+144
View File
@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pair_incorrect_pin">Неправилен ПИН</string>
<string name="no_video_received_error">Няма получено видео от хоста.</string>
<string name="pacing_latency">Предпочитане на най-ниската латентност</string>
<string name="pacing_balanced_alt">Балансирано с FPS лимит</string>
<string name="pacing_smoothness">Предпочитане на най-плавно видео (може значително да увеличи латентността)</string>
<string name="conn_metered">Предупреждение: Вашата активна мрежова връзка се измерва!</string>
<string name="conn_client_latency_hw">латентност на хардуерния декодер:</string>
<string name="conn_hardware_latency">Средна латентност на хардуерно декодиране:</string>
<string name="ip_hint">IP адрес на GeForce PC</string>
<string name="pcview_menu_send_wol">Изпращане на Wake-On-LAN заявка</string>
<string name="pcview_menu_delete_pc">Изтрий компютъра</string>
<string name="pcview_menu_test_network">Тестване на мрежовата връзка</string>
<string name="pcview_menu_details">Детайли</string>
<string name="nettest_title_waiting">Тестване на мрежовата връзка</string>
<string name="nettest_title_done">Тестът на мрежата е завършен</string>
<string name="pairing">Сдвояване…</string>
<string name="pair_pc_offline">Компютърът е офлайн</string>
<string name="pair_pc_ingame">Компютърът в момента е в игра. Трябва да затворите играта преди сдвояване.</string>
<string name="pair_pairing_title">Сдвояване</string>
<string name="wol_waking_pc">Събуждащ се компютъра…</string>
<string name="unpair_fail">Неуспешно раздвояване</string>
<string name="unpair_error">Устройството не беше сдвоено</string>
<string name="error_pc_offline">Компютърът е офлайн</string>
<string name="title_decoding_error">Видеодекодерът крашна</string>
<string name="no_frame_received_error">Мрежовата ви връзка не работи добре. Намалете настройката си за битрейт на видео или опитайте с по-бърза връзка.</string>
<string name="conn_client_latency">Средна латентност при декодиране на кадър:</string>
<string name="conn_starting">Стартиране</string>
<string name="conn_terminated_title">Връзката е прекратена</string>
<string name="yes">Да</string>
<string name="applist_menu_hide_app">Скриване на приложението</string>
<string name="applist_refresh_title">Списък с приложения</string>
<string name="summary_resolution_list">Увеличете, за да подобрите яснотата на изображението. Намалете за по-добра производителност на устройства от по-нисък клас и по-бавни мрежи.</string>
<string name="title_fps_list">Кадрова честота на видео</string>
<string name="scut_deleted_pc">Компютърът е изтрит</string>
<string name="scut_not_paired">Компютърът не е сдвоен</string>
<string name="scut_pc_not_found">Компютърът не е намерен</string>
<string name="scut_invalid_uuid">Предоставеният компютър не е валиден</string>
<string name="scut_invalid_app_id">Предоставеното приложение не е валидно</string>
<string name="help_loading_msg">Помощната страница се зарежда…</string>
<string name="help_loading_title">Помощ</string>
<string name="pcview_menu_header_online">Онлайн</string>
<string name="pcview_menu_header_unknown">Презареждане</string>
<string name="pcview_menu_pair_pc">Сдвояване с компютър</string>
<string name="pcview_menu_header_offline">Извън линия</string>
<string name="pcview_menu_app_list">Вижте Всички Приложения</string>
<string name="pcview_menu_unpair_pc">Раздвояване</string>
<string name="nettest_text_waiting">Moonlight тества вашата мрежова връзка, за да определи дали NVIDIA GameStream е блокиран.
\n
\nТова може да отнеме няколко секунди…</string>
<string name="nettest_text_success">Вашата мрежа изглежда не блокира Moonlight. Ако все още имате проблеми със свързването, проверете настройките на защитната стена на вашия компютър.
\n
\nАко се опитвате да стриймвате през интернет, инсталирайте Moonlight Internet Hosting Tool на вашия компютър и стартирайте включения тестер за интернет стрийминг, за да проверите интернет връзката на вашия компютър.</string>
<string name="nettest_text_inconclusive">Мрежовият тест не можа да бъде извършен, защото нито един от сървърите за тестване на връзката на Moonlight не е достъпен. Проверете връзката си с интернет или опитайте отново по-късно.</string>
<string name="nettest_text_failure">Текущата мрежова връзка на вашето устройство изглежда блокира Moonlight. Стриймването през интернет може да не работи, докато сте свързани към тази мрежа.
\n
\nСледните мрежови портове са блокирани:
\n</string>
<string name="nettest_text_blocked">Текущата мрежова връзка на вашето устройство блокира Moonlight. Стриймването през интернет може да не работи, докато сте свързани към тази мрежа.</string>
<string name="pair_pairing_msg">Моля, въведете следния ПИН на избрания компютър:</string>
<string name="pair_fail">Неуспешно сдвояване</string>
<string name="pair_already_in_progress">Сдвояването вече е в ход</string>
<string name="wol_pc_online">Компютърът е онлайн</string>
<string name="wol_no_mac">Компютърът не може да бъде събуден, защото GFE не изпрати MAC адрес</string>
<string name="wol_fail">Неуспешно изпращане на Wake-On-LAN пакети</string>
<string name="wol_waking_msg">Може да отнеме няколко секунди, докато вашият компютър се събуди. Ако не стане, уверете се, че е конфигуриран правилно за Wake-On-LAN.</string>
<string name="unpairing">Раздвояване…</string>
<string name="unpair_success">Раздвояването бе успешно</string>
<string name="video_decoder_init_failed">Видео декодерът не успя да се инициализира. Вашето устройство може да не поддържа избраната резолюция или честота.</string>
<string name="error_manager_not_running">Услугата ComputerManager не работи. Моля, изчакайте няколко секунди или рестартирайте приложението.</string>
<string name="error_404">GFE върна грешка HTTP 404. Уверете се, че вашият компютър има поддръжана видео карта. Използването на софтуер за отдалечен работен плот също може да причини тази грешка. Опитайте да рестартирате машината си или да преинсталирате GFE.</string>
<string name="message_decoding_error">Moonlight претърпя срив поради несъвместимост с видеодекодера на това устройство. Уверете се, че GeForce Experience е актуализиран до най-новата версия на вашия компютър. Опитайте да коригирате настройките за стриймване, ако сривовете продължат.</string>
<string name="title_decoding_reset">Нулиране на видео настройките</string>
<string name="message_decoding_reset">Видео декодерът на вашето устройство продължава да се срива при избраните от вас настройки за стриймване. Настройките са нулирани по подразбиране.</string>
<string name="audioconf_stereo">Стерео</string>
<string name="applist_refresh_msg">Приложенията се опресняват…</string>
<string name="error_usb_prohibited">USB достъпът е забранен от администратора на вашето устройство. Проверете настройките на Knox или MDM.</string>
<string name="audioconf_71surround">7.1 съраунд звук</string>
<string name="audioconf_51surround">5.1 съраунд звук</string>
<string name="videoformat_hevcauto">Автоматично</string>
<string name="videoformat_hevcalways">Винаги използване на HEVC (може да крашне)</string>
<string name="videoformat_hevcnever">Никога да не се използва HEVC</string>
<string name="summary_frame_pacing">Посочете как да се балансира забавянето и плавността на видеото</string>
<string name="title_frame_pacing">Стъпка на видео кадрите</string>
<string name="pacing_balanced">Балансирано</string>
<string name="check_ports_msg">Проверете вашата защитна стена и правилата за препращане на портове за порт(ове):</string>
<string name="conn_establishing_title">Установяване на връзка</string>
<string name="conn_establishing_msg">Стартиране на връзка</string>
<string name="conn_error_msg">Неуспешно стартиране</string>
<string name="conn_terminated_msg">Връзката беше прекратена</string>
<string name="conn_error_title">Грешка при свързване</string>
<string name="no">Не</string>
<string name="help">Помощ</string>
<string name="lost_connection">Загубена връзка с компютър</string>
<string name="title_details">Подробности</string>
<string name="delete_pc_msg">Сигурни ли сте, че искате да изтриете този компютър\?</string>
<string name="poor_connection_msg">Лоша връзка с компютъра</string>
<string name="perf_overlay_streamdetails">Видео поток: %1$s %2$.2f FPS</string>
<string name="perf_overlay_decoder">Декодер: %1$s</string>
<string name="perf_overlay_netdrops">Кадри, пропуснати от вашата мрежова връзка: %1$.2f%%</string>
<string name="perf_overlay_incomingfps">Входяща честота на кадрите от мрежата: %1$.2f FPS</string>
<string name="perf_overlay_netlatency">Средно забавяне на мрежата: %1$d ms (variance: %2$d ms)</string>
<string name="applist_connect_msg">Свързване с компютъра…</string>
<string name="perf_overlay_renderingfps">Кадрова честота на изобразяване: %1$.2f FPS</string>
<string name="perf_overlay_dectime">Средно време за декодиране: %1$.2f ms</string>
<string name="applist_menu_quit">Прекратяване на сесията</string>
<string name="msg_add_pc">Свързване към компютъра…</string>
<string name="applist_menu_resume">Възобновяване на сесията</string>
<string name="applist_menu_quit_and_start">Изключване на текущата игра и стартиране</string>
<string name="applist_menu_cancel">Отказ</string>
<string name="applist_menu_details">Виж детайлите</string>
<string name="applist_menu_scut">Създаване на пряк път</string>
<string name="applist_refresh_error_title">Грешка</string>
<string name="applist_menu_tv_channel">Добавяне към канал</string>
<string name="applist_refresh_error_msg">Неуспешно получаване на списък с приложения</string>
<string name="applist_quit_success">Успешно изключване</string>
<string name="applist_quit_app">Изключване</string>
<string name="applist_quit_fail">Неуспешно изключване</string>
<string name="applist_details_id">ID на приложението:</string>
<string name="applist_quit_confirmation">Сигурни ли сте, че искате да затворите работещото приложение\? Всички незапазени данни ще бъдат загубени.</string>
<string name="title_add_pc">Ръчно добавяне на компютър</string>
<string name="addpc_fail">Неуспешна връзка с посочения компютър. Уверете се, че необходимите портове са разрешени през защитната стена.</string>
<string name="category_basic_settings">Основни настройки</string>
<string name="addpc_success">Успешно добавен компютър</string>
<string name="addpc_enter_ip">Трябва да въведете IP адрес</string>
<string name="addpc_wrong_sitelocal">Този адрес не изглежда правилен. Трябва да използвате публичния IP адрес на вашия рутер за стриймване през интернет.</string>
<string name="title_resolution_list">Видео резолюция</string>
<string name="summary_fps_list">Увеличете за по-плавен видео поток. Намалете за по-добра производителност на устройства от по-нисък клас.</string>
<string name="title_seekbar_bitrate">Видео битрейт</string>
<string name="suffix_seekbar_bitrate_mbps">Mbps</string>
<string name="title_audio_config_list">Конфигурация на съраунд звук</string>
<string name="summary_audio_config_list">Активиране на 5.1 или 7.1 съраунд звук за системи за домашно кино</string>
<string name="summary_seekbar_bitrate">Увеличете за по-добро качество на изображението. Намалете, за да подобрите производителността при по-бавни връзки.</string>
<string name="resolution_prefix_native_portrait">(Портрет)</string>
<string name="title_checkbox_enable_audiofx">Активиране на поддръжката на системния еквалайзер</string>
<string name="title_checkbox_stretch_video">Разтегляне на видеото на цял екран</string>
<string name="resolution_prefix_native_landscape">(Пейзаж)</string>
<string name="category_audio_settings">Аудио настройки</string>
<string name="category_input_settings">Настройки за въвеждане</string>
<string name="title_checkbox_touchscreen_trackpad">Използване на сензорния екран като тракпад</string>
<string name="title_checkbox_multi_controller">Автоматично откриване на наличен контролер</string>
<string name="summary_checkbox_multi_controller">Премахването на отметката от тази опция принуждава контролер винаги да присъства</string>
</resources>
+3
View File
@@ -259,4 +259,7 @@
<string name="title_checkbox_reduce_refresh_rate">Aktualisierungsrate verringern erlauben</string>
<string name="summary_checkbox_reduce_refresh_rate">Durch das Verringern der Display Aktualisierungsrate, kann, auf Kosten der Video-Latenz, der Akkuverbrauch reduziert werden</string>
<string name="resolution_prefix_native_landscape">(Landscape)</string>
<string name="frame_conversion_error">Der Host-PC hat einen schwerwiegenden Videocodierungsfehler gemeldet.
\n
\nVersuchen Sie, den HDR-Modus zu deaktivieren, die Streaming-Auflösung zu ändern oder die Bildschirmauflösung des Host-PCs zu ändern.</string>
</resources>
+6
View File
@@ -253,4 +253,10 @@
<string name="title_checkbox_absolute_mouse_mode">Mode souris pour bureau à distance</string>
<string name="summary_seekbar_deadzone">Remarque : Certains jeux peuvent imposer une zone morte plus grande que celle que Moonlight est configuré pour utiliser.</string>
<string name="summary_checkbox_absolute_mouse_mode">Cela peut rendre l\'accélération de la souris plus naturelle pour l\'utilisation du bureau à distance, mais elle est incompatible avec de nombreux jeux.</string>
<string name="resolution_prefix_native_landscape">(Paysage)</string>
<string name="resolution_prefix_native_portrait">(Portrait)</string>
<string name="title_checkbox_reduce_refresh_rate">Autoriser la réduction du taux de rafraîchissement</string>
<string name="summary_checkbox_reduce_refresh_rate">Des taux de rafraîchissement d\'affichage plus bas peuvent économiser de l\'énergie au détriment d\'une latence vidéo plus importante</string>
<string name="summary_checkbox_enable_audiofx">Permet aux effets audio de fonctionner lors du streaming, mais peut augmenter la latence</string>
<string name="title_checkbox_enable_audiofx">Activer le support de l\'égalisateur système</string>
</resources>
+4
View File
@@ -255,4 +255,8 @@
<string name="title_frame_pacing">비디오 프레임 처리방식</string>
<string name="pacing_smoothness">가장 부드러운 비디오 선호(대기 시간이 크게 증가할 수 있음)</string>
<string name="category_help">도움말</string>
<string name="resolution_prefix_native_landscape">(가로)</string>
<string name="resolution_prefix_native_portrait">(세로)</string>
<string name="title_checkbox_reduce_refresh_rate">주사율 감소 허용</string>
<string name="summary_checkbox_reduce_refresh_rate">화면 주사율을 낮춰서 영상 지연 시간이 증가하고 전력을 절약할 수 있습니다.</string>
</resources>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="scut_deleted_pc">PC usunięty</string>
<string name="scut_pc_not_found">PC nie znaleziony</string>
<string name="pcview_menu_header_unknown">Odświeżanie</string>
<string name="scut_not_paired">PC niesparowany</string>
</resources>
+7 -1
View File
@@ -217,7 +217,7 @@
<string name="nettest_text_blocked">您设备当前的网络连接拦截了Moonlight。连接到该网络时可能无法通过互联网串流。</string>
<string name="perf_overlay_netlatency">平均网络延迟: %1$d ms (抖动: %2$d ms)</string>
<string name="perf_overlay_streamdetails">视频流: %1$s %2$.2f FPS</string>
<string name="resolution_prefix_native_fullscreen">本地全屏</string>
<string name="resolution_prefix_native_fullscreen">原生全屏</string>
<!-- Array strings -->
<string name="audioconf_stereo">立体声</string>
<string name="audioconf_51surround">5.1环绕声</string>
@@ -251,4 +251,10 @@
<string name="summary_seekbar_deadzone">注意:有些游戏可以执行一个比Moonlight摇杆配置的更大的盲区。</string>
<string name="title_checkbox_absolute_mouse_mode">适合远程桌面的鼠标模式</string>
<string name="summary_checkbox_absolute_mouse_mode">这可以使得鼠标加速在远程桌面使用中表现得更自然,但它与许多游戏不兼容。</string>
<string name="resolution_prefix_native_landscape">(横向)</string>
<string name="resolution_prefix_native_portrait">(纵向)</string>
<string name="title_checkbox_enable_audiofx">启用对系统均衡器的支持</string>
<string name="summary_checkbox_enable_audiofx">串流时允许音效工作,可能会导致音频延迟增加</string>
<string name="title_checkbox_reduce_refresh_rate">允许降低刷新率</string>
<string name="summary_checkbox_reduce_refresh_rate">较低的屏幕刷新率可以在增加一些视频延迟的情况下省电</string>
</resources>
@@ -254,4 +254,8 @@
<string name="summary_checkbox_absolute_mouse_mode">這可以讓滑鼠在遠端桌面使用中的加速表現更加自然,但與很多遊戲不相容。</string>
<string name="title_checkbox_enable_audiofx">啟用系統等化器支援</string>
<string name="summary_checkbox_enable_audiofx">允許音訊效果在串流中發揮作用,但可能會增加音訊延遲</string>
<string name="resolution_prefix_native_landscape">(橫向)</string>
<string name="resolution_prefix_native_portrait">(直向)</string>
<string name="title_checkbox_reduce_refresh_rate">允許減小重新整理率</string>
<string name="summary_checkbox_reduce_refresh_rate">更低的顯示器重新整理速率可在犧牲一些額外視訊延遲的狀況下節省電力</string>
</resources>
+1
View File
@@ -74,6 +74,7 @@
<string name="no_video_received_error">No video received from host.</string>
<string name="no_frame_received_error">Your network connection isn\'t performing well. Reduce your video bitrate setting or try a faster connection.</string>
<string name="early_termination_error">Something went wrong on your host PC when starting the stream.\n\nMake sure you don\'t have any DRM-protected content open on your host PC. You can also try restarting your host PC.\n\nIf the issue persists, try reinstalling your GPU drivers and GeForce Experience.</string>
<string name="frame_conversion_error">The host PC reported a fatal video encoding error.\n\nTry disabling HDR mode, changing the streaming resolution, or changing your host PC\'s display resolution.</string>
<string name="check_ports_msg">Check your firewall and port forwarding rules for port(s):</string>
<!-- Start application messages -->
@@ -0,0 +1,3 @@
- Fixed several input bugs with the on-screen gamepad
- Implemented recovery logic for video decoder errors
- Updated community contributed translations from Weblate
@@ -0,0 +1 @@
- Added support for GeForce Experience 3.26
@@ -0,0 +1,6 @@
- Qualcomm devices now use HEVC by default for improved efficiency
- Added system key capture on Samsung devices running Android 10 or later
- Improved frame loss handling when using HEVC
- Fixed streaming crash on devices running Android 4.1 to 4.4
- Fixed streaming at resolutions below 720x540 with GeForce Experience 3.26
- Updated community contributed translations from Weblate