Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8ea2a8ec1 | |||
| 9ed3b3a9df | |||
| 12487553de | |||
| 9c1a618b4a | |||
| ac0e784417 | |||
| 48cab6b203 | |||
| e1c0472069 | |||
| 2c498ce707 | |||
| bc483edb29 | |||
| 9762f4c412 | |||
| 5bfce88fc5 | |||
| 94ef66994d | |||
| 257c29daca | |||
| 173483eb84 | |||
| 06099b2663 | |||
| 33c1f0a71c | |||
| a3d78f1d80 | |||
| c573d213f8 | |||
| c72707aef9 | |||
| 313ef06c86 | |||
| 6b79340c15 | |||
| d9a5b29372 | |||
| d2b0e093fc | |||
| 945e563912 | |||
| a7efa379eb | |||
| d04df4ebe5 | |||
| 2a2c84ef3a | |||
| bc97db893a | |||
| f216834df7 | |||
| be25a7d594 | |||
| 10f43e8024 | |||
| bbb3e8d071 | |||
| 4c3af35156 | |||
| 8656228014 | |||
| 03f9ea8435 | |||
| 9cf27d8fb1 | |||
| d1b24ea6af | |||
| b07ffbde29 | |||
| 1673236940 | |||
| 06861a2d17 | |||
| ef7ac62f97 | |||
| 245a9f2751 | |||
| 1d38f158b5 | |||
| 62a526854d | |||
| 3dda940c92 | |||
| ab77c4720d | |||
| c8f1f9325e | |||
| 658940d3fb | |||
| 51b4ca401e | |||
| 10e4ca4ef3 | |||
| 2bcc2bdfe5 | |||
| 6462b580bb | |||
| b83d91c944 | |||
| 07f842bc9e | |||
| 3913e845fa | |||
| 09f0913974 | |||
| aa9ca35115 | |||
| 010dfdf834 | |||
| 150fac9c09 | |||
| ec3aef13d8 | |||
| 2b56005bd2 | |||
| 9bc893b6ad | |||
| 3feb92e788 | |||
| 1265952814 | |||
| f5ad5d97db | |||
| 5ac0939731 | |||
| b653694860 | |||
| 49051a5095 | |||
| 7734de6465 | |||
| edac646434 | |||
| 51c7665fdc | |||
| 37545821fc | |||
| 8a1ed0f146 | |||
| 7a0228fb81 | |||
| 3aecf9e031 | |||
| c2d4d221af | |||
| 53f89fbe22 |
+2
-2
@@ -9,8 +9,8 @@ android {
|
||||
minSdk 16
|
||||
targetSdk 33
|
||||
|
||||
versionName "10.7"
|
||||
versionCode = 286
|
||||
versionName "10.8.4"
|
||||
versionCode = 293
|
||||
|
||||
// Generate native debug symbols to allow Google Play to symbolicate our native crashes
|
||||
ndk.debugSymbolLevel = 'FULL'
|
||||
|
||||
@@ -124,11 +124,9 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
|
||||
<activity
|
||||
android:name=".Game"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:noHistory="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:resizeableActivity="true"
|
||||
|
||||
@@ -199,12 +199,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
|
||||
}
|
||||
|
||||
// We specified userLandscape in the manifest which isn't supported until 4.3,
|
||||
// so we must fall back at runtime to sensorLandscape.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
}
|
||||
|
||||
// Listen for UI visibility events
|
||||
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
|
||||
|
||||
@@ -222,6 +216,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
||||
|
||||
// Enter landscape unless we're on a square screen
|
||||
setPreferredOrientationForCurrentDisplay();
|
||||
|
||||
if (prefConfig.stretchVideo || shouldIgnoreInsetsForResolution(prefConfig.width, prefConfig.height)) {
|
||||
// Allow the activity to layout under notches if the fill-screen option
|
||||
// was turned on by the user or it's a full-screen native resolution
|
||||
@@ -235,12 +232,36 @@ 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.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.
|
||||
// Without this, input events are buffered to be delivered in lock-step with VBlank,
|
||||
// artificially increasing input latency while streaming.
|
||||
streamView.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.
|
||||
needsInputBatching = true;
|
||||
}
|
||||
|
||||
notificationOverlayView = findViewById(R.id.notificationOverlay);
|
||||
|
||||
performanceOverlayView = findViewById(R.id.performanceOverlay);
|
||||
@@ -453,7 +474,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
.build();
|
||||
|
||||
// Initialize the connection
|
||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert);
|
||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert, needsInputBatching);
|
||||
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
||||
keyboardTranslator = new KeyboardTranslator();
|
||||
|
||||
@@ -510,10 +531,72 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
streamView.getHolder().addCallback(this);
|
||||
}
|
||||
|
||||
private void setPreferredOrientationForCurrentDisplay() {
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
|
||||
// For semi-square displays, we use more complex logic to determine which orientation to use (if any)
|
||||
if (PreferenceConfiguration.isSquarishScreen(display)) {
|
||||
int desiredOrientation = Configuration.ORIENTATION_UNDEFINED;
|
||||
|
||||
// OSC doesn't properly support portrait displays, so don't use it in portrait mode by default
|
||||
if (prefConfig.onscreenController) {
|
||||
desiredOrientation = Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
// For native resolution, we will lock the orientation to the one that matches the specified resolution
|
||||
if (PreferenceConfiguration.isNativeResolution(prefConfig.width, prefConfig.height)) {
|
||||
if (prefConfig.width > prefConfig.height) {
|
||||
desiredOrientation = Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
else {
|
||||
desiredOrientation = Configuration.ORIENTATION_PORTRAIT;
|
||||
}
|
||||
}
|
||||
|
||||
if (desiredOrientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE);
|
||||
}
|
||||
else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
}
|
||||
}
|
||||
else if (desiredOrientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT);
|
||||
}
|
||||
else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If we don't have a reason to lock to portrait or landscape, allow any orientation
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
|
||||
}
|
||||
else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// For regular displays, we always request landscape
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE);
|
||||
}
|
||||
else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
// Set requested orientation for possible new screen size
|
||||
setPreferredOrientationForCurrentDisplay();
|
||||
|
||||
if (virtualController != null) {
|
||||
// Refresh layout of OSC for possible new screen size
|
||||
virtualController.refreshLayout();
|
||||
@@ -701,6 +784,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean mayReduceRefreshRate() {
|
||||
return prefConfig.framePacing == PreferenceConfiguration.FRAME_PACING_CAP_FPS ||
|
||||
prefConfig.framePacing == PreferenceConfiguration.FRAME_PACING_MAX_SMOOTHNESS ||
|
||||
(prefConfig.framePacing == PreferenceConfiguration.FRAME_PACING_BALANCED && prefConfig.reduceRefreshRate);
|
||||
}
|
||||
|
||||
private float prepareDisplayForRendering() {
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||
@@ -712,7 +801,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
boolean isNativeResolutionStream = PreferenceConfiguration.isNativeResolution(prefConfig.width, prefConfig.height);
|
||||
boolean refreshRateIsGood = isRefreshRateGoodMatch(bestMode.getRefreshRate());
|
||||
boolean refreshRateIsEqual = isRefreshRateEqualMatch(bestMode.getRefreshRate());
|
||||
boolean isTelevision = getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
|
||||
|
||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||
boolean refreshRateReduced = candidate.getRefreshRate() < bestMode.getRefreshRate();
|
||||
@@ -745,8 +833,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prefConfig.framePacing != PreferenceConfiguration.FRAME_PACING_MIN_LATENCY &&
|
||||
refreshRateIsEqual && !isRefreshRateEqualMatch(candidate.getRefreshRate())) {
|
||||
if (mayReduceRefreshRate() && refreshRateIsEqual && !isRefreshRateEqualMatch(candidate.getRefreshRate())) {
|
||||
// If we had an equal refresh rate and this one is not, skip it. In min latency
|
||||
// mode, we want to always prefer the highest frame rate even though it may cause
|
||||
// microstuttering.
|
||||
@@ -759,21 +846,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
continue;
|
||||
}
|
||||
|
||||
// We don't want ever reduce our refresh rate unless we found an exact
|
||||
// match and we're not in min latency mode.
|
||||
if (refreshRateReduced) {
|
||||
if (prefConfig.framePacing == PreferenceConfiguration.FRAME_PACING_MIN_LATENCY) {
|
||||
if (mayReduceRefreshRate()) {
|
||||
// User asked for the lowest possible refresh rate, so don't raise it if we
|
||||
// have a good match already
|
||||
if (candidate.getRefreshRate() > bestMode.getRefreshRate()) {
|
||||
continue;
|
||||
}
|
||||
else if (!isRefreshRateEqualMatch(candidate.getRefreshRate())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For refresh rates lower than 50hz, we want to check if the device is a TV.
|
||||
// Some TV's may have issues when attempting to lower its refresh rate
|
||||
// As opposed to mobile devices, which are designed to lower refresh rate
|
||||
// for battery life reasons.
|
||||
else if(isTelevision && candidate.getRefreshRate() < 50) {
|
||||
}
|
||||
else {
|
||||
// User asked for the highest possible refresh rate, so don't reduce it if we
|
||||
// have a good match already
|
||||
if (refreshRateReduced) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1316,7 +1399,27 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER)) ||
|
||||
eventSource == 12290) // 12290 = Samsung DeX mode desktop mouse
|
||||
{
|
||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||
int buttonState = event.getButtonState();
|
||||
int changedButtons = buttonState ^ lastButtonState;
|
||||
|
||||
// The DeX touchpad on the Fold 4 sends proper right click events using BUTTON_SECONDARY,
|
||||
// but doesn't send BUTTON_PRIMARY for a regular click. Instead it sends ACTION_DOWN/UP,
|
||||
// so we need to fix that up to look like a sane input event to process it correctly.
|
||||
if (eventSource == 12290) {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
buttonState |= MotionEvent.BUTTON_PRIMARY;
|
||||
}
|
||||
else if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
buttonState &= ~MotionEvent.BUTTON_PRIMARY;
|
||||
}
|
||||
else {
|
||||
// We may be faking the primary button down from a previous event,
|
||||
// so be sure to add that bit back into the button state.
|
||||
buttonState |= (lastButtonState & MotionEvent.BUTTON_PRIMARY);
|
||||
}
|
||||
|
||||
changedButtons = buttonState ^ lastButtonState;
|
||||
}
|
||||
|
||||
// Ignore mouse input if we're not capturing from our input source
|
||||
if (!inputCaptureProvider.isCapturingActive()) {
|
||||
@@ -1379,7 +1482,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||
if ((buttonState & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
else {
|
||||
@@ -1389,7 +1492,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// Mouse secondary or stylus primary is right click (stylus down is left click)
|
||||
if ((changedButtons & (MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) {
|
||||
if ((event.getButtonState() & (MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) {
|
||||
if ((buttonState & (MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_STYLUS_PRIMARY)) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||
}
|
||||
else {
|
||||
@@ -1399,7 +1502,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// Mouse tertiary or stylus secondary is middle click
|
||||
if ((changedButtons & (MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_SECONDARY)) != 0) {
|
||||
if ((event.getButtonState() & (MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_SECONDARY)) != 0) {
|
||||
if ((buttonState & (MotionEvent.BUTTON_TERTIARY | MotionEvent.BUTTON_STYLUS_SECONDARY)) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
|
||||
}
|
||||
else {
|
||||
@@ -1409,7 +1512,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
if (prefConfig.mouseNavButtons) {
|
||||
if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) {
|
||||
if ((buttonState & MotionEvent.BUTTON_BACK) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1);
|
||||
}
|
||||
else {
|
||||
@@ -1418,7 +1521,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||
if ((buttonState & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
|
||||
}
|
||||
else {
|
||||
@@ -1465,7 +1568,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
lastButtonState = event.getButtonState();
|
||||
lastButtonState = buttonState;
|
||||
}
|
||||
// This case is for fingers
|
||||
else
|
||||
@@ -1477,15 +1580,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 &&
|
||||
@@ -1540,7 +1650,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:
|
||||
@@ -1553,8 +1666,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));
|
||||
}
|
||||
}
|
||||
@@ -1565,8 +1678,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());
|
||||
}
|
||||
}
|
||||
@@ -1590,22 +1703,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 ||
|
||||
@@ -1638,10 +1756,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
|
||||
@@ -1652,6 +1770,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent event) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
// Tell the OS not to buffer input events for us
|
||||
//
|
||||
// NB: This is still needed even when we call the newer requestUnbufferedDispatch()!
|
||||
view.requestUnbufferedDispatch(event);
|
||||
}
|
||||
}
|
||||
|
||||
return handleMotionEvent(view, event);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+11
-38
@@ -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();
|
||||
|
||||
+31
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-13
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
+11
-4
@@ -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,9 +1,10 @@
|
||||
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;
|
||||
@@ -48,6 +49,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
private boolean submittedCsd;
|
||||
private boolean submitCsdNextCall;
|
||||
|
||||
private int nextInputBufferIndex = -1;
|
||||
private ByteBuffer nextInputBuffer;
|
||||
|
||||
private Context context;
|
||||
private MediaCodec videoDecoder;
|
||||
private Thread rendererThread;
|
||||
@@ -68,6 +72,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;
|
||||
@@ -334,66 +355,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();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -406,7 +429,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;
|
||||
}
|
||||
@@ -459,7 +482,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;
|
||||
}
|
||||
@@ -470,26 +494,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.
|
||||
@@ -510,15 +741,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
|
||||
@@ -550,13 +779,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);
|
||||
}
|
||||
@@ -643,7 +882,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
|
||||
@@ -671,8 +916,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,19 +929,54 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
rendererThread.start();
|
||||
}
|
||||
|
||||
private int dequeueInputBuffer() {
|
||||
int index = -1;
|
||||
private boolean fetchNextInputBuffer() {
|
||||
long startTime;
|
||||
boolean codecRecovered;
|
||||
|
||||
if (nextInputBuffer != null) {
|
||||
// We already have an input buffer
|
||||
return true;
|
||||
}
|
||||
|
||||
startTime = SystemClock.uptimeMillis();
|
||||
|
||||
try {
|
||||
while (index < 0 && !stopping) {
|
||||
index = videoDecoder.dequeueInputBuffer(10000);
|
||||
// If we don't have an input buffer index yet, fetch one now
|
||||
while (nextInputBufferIndex < 0 && !stopping) {
|
||||
nextInputBufferIndex = videoDecoder.dequeueInputBuffer(10000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, 0, true);
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
|
||||
// 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];
|
||||
|
||||
// Clear old input data pre-Lollipop
|
||||
nextInputBuffer.clear();
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
int deltaMs = (int)(SystemClock.uptimeMillis() - startTime);
|
||||
@@ -703,7 +985,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
LimeLog.warning("Dequeue input buffer ran long: " + deltaMs + " ms");
|
||||
}
|
||||
|
||||
if (index < 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) {
|
||||
@@ -714,10 +996,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
}
|
||||
throw new RendererException(this, decoderHungException);
|
||||
}
|
||||
return index;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return index;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -736,6 +1019,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() {
|
||||
@@ -793,39 +1082,48 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
// TODO: Set HDR metadata?
|
||||
}
|
||||
|
||||
private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
|
||||
private boolean queueNextInputBuffer(long timestampUs, int codecFlags) {
|
||||
boolean codecRecovered;
|
||||
|
||||
try {
|
||||
videoDecoder.queueInputBuffer(inputBufferIndex,
|
||||
offset, length,
|
||||
videoDecoder.queueInputBuffer(nextInputBufferIndex,
|
||||
0, nextInputBuffer.position(),
|
||||
timestampUs, codecFlags);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, codecFlags, true);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Using the new getInputBuffer() API on Lollipop allows
|
||||
// the framework to do some performance optimizations for us
|
||||
private ByteBuffer getEmptyInputBuffer(int inputBufferIndex) {
|
||||
ByteBuffer buf;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
buf = videoDecoder.getInputBuffer(inputBufferIndex);
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, 0, true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
buf = legacyInputBuffers[inputBufferIndex];
|
||||
|
||||
// Clear old input data pre-Lollipop
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
return buf;
|
||||
// Fetch a new input buffer now while we have some time between frames
|
||||
// to have it ready immediately when the next frame arrives.
|
||||
//
|
||||
// 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) {
|
||||
@@ -904,19 +1202,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
activeWindowVideoStats.measurementStartTimestamp = SystemClock.uptimeMillis();
|
||||
}
|
||||
|
||||
int inputBufferIndex;
|
||||
ByteBuffer buf;
|
||||
long timestampUs;
|
||||
int codecFlags = 0;
|
||||
|
||||
// H264 SPS
|
||||
if (decodeUnitData[4] == 0x67) {
|
||||
if (decodeUnitType == MoonBridge.BUFFER_TYPE_SPS && (videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
|
||||
numSpsIn++;
|
||||
|
||||
ByteBuffer spsBuf = ByteBuffer.wrap(decodeUnitData);
|
||||
int startSeqLen = decodeUnitData[2] == 0x01 ? 3 : 4;
|
||||
|
||||
// Skip to the start of the NALU data
|
||||
spsBuf.position(5);
|
||||
spsBuf.position(startSeqLen + 1);
|
||||
|
||||
// The H264Utils.readSPS function safely handles
|
||||
// Annex B NALUs (including NALUs with escape sequences)
|
||||
@@ -963,7 +1260,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;
|
||||
@@ -975,6 +1272,12 @@ 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");
|
||||
@@ -999,7 +1302,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
// 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;
|
||||
@@ -1020,9 +1323,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, decodeUnitLength);
|
||||
|
||||
// Batch this to submit together with PPS
|
||||
spsBuffer = new byte[5 + escapedNalu.limit()];
|
||||
System.arraycopy(decodeUnitData, 0, spsBuffer, 0, 5);
|
||||
escapedNalu.get(spsBuffer, 5, escapedNalu.limit());
|
||||
spsBuffer = new byte[startSeqLen + 1 + escapedNalu.limit()];
|
||||
System.arraycopy(decodeUnitData, 0, spsBuffer, 0, startSeqLen + 1);
|
||||
escapedNalu.get(spsBuffer, startSeqLen + 1, escapedNalu.limit());
|
||||
return MoonBridge.DR_OK;
|
||||
}
|
||||
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_VPS) {
|
||||
@@ -1049,25 +1352,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
// fused IDR frames, we will submit the CSD blob in a
|
||||
// separate input buffer.
|
||||
if (!submittedCsd || !fusedIdrFrame) {
|
||||
inputBufferIndex = dequeueInputBuffer();
|
||||
if (inputBufferIndex < 0) {
|
||||
// We're being torn down now
|
||||
return MoonBridge.DR_NEED_IDR;
|
||||
}
|
||||
|
||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||
if (buf == null) {
|
||||
// We're being torn down now
|
||||
if (!fetchNextInputBuffer()) {
|
||||
return MoonBridge.DR_NEED_IDR;
|
||||
}
|
||||
|
||||
// When we get the PPS, submit the VPS and SPS together with
|
||||
// the PPS, as required by AOSP docs on use of MediaCodec.
|
||||
if (vpsBuffer != null) {
|
||||
buf.put(vpsBuffer);
|
||||
nextInputBuffer.put(vpsBuffer);
|
||||
}
|
||||
if (spsBuffer != null) {
|
||||
buf.put(spsBuffer);
|
||||
nextInputBuffer.put(spsBuffer);
|
||||
}
|
||||
|
||||
// This is the CSD blob
|
||||
@@ -1096,27 +1391,19 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
activeWindowVideoStats.totalTimeMs += enqueueTimeMs - receiveTimeMs;
|
||||
}
|
||||
|
||||
inputBufferIndex = dequeueInputBuffer();
|
||||
if (inputBufferIndex < 0) {
|
||||
// We're being torn down now
|
||||
return MoonBridge.DR_NEED_IDR;
|
||||
}
|
||||
|
||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||
if (buf == null) {
|
||||
// We're being torn down now
|
||||
if (!fetchNextInputBuffer()) {
|
||||
return MoonBridge.DR_NEED_IDR;
|
||||
}
|
||||
|
||||
if (submitCsdNextCall) {
|
||||
if (vpsBuffer != null) {
|
||||
buf.put(vpsBuffer);
|
||||
nextInputBuffer.put(vpsBuffer);
|
||||
}
|
||||
if (spsBuffer != null) {
|
||||
buf.put(spsBuffer);
|
||||
nextInputBuffer.put(spsBuffer);
|
||||
}
|
||||
if (ppsBuffer != null) {
|
||||
buf.put(ppsBuffer);
|
||||
nextInputBuffer.put(ppsBuffer);
|
||||
}
|
||||
|
||||
submitCsdNextCall = false;
|
||||
@@ -1139,9 +1426,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
numFramesIn++;
|
||||
}
|
||||
|
||||
if (decodeUnitLength > buf.limit() - buf.position()) {
|
||||
if (decodeUnitLength > nextInputBuffer.limit() - nextInputBuffer.position()) {
|
||||
IllegalArgumentException exception = new IllegalArgumentException(
|
||||
"Decode unit length "+decodeUnitLength+" too large for input buffer "+buf.limit());
|
||||
"Decode unit length "+decodeUnitLength+" too large for input buffer "+nextInputBuffer.limit());
|
||||
if (!reportedCrash) {
|
||||
reportedCrash = true;
|
||||
crashListener.notifyCrash(exception);
|
||||
@@ -1150,11 +1437,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
}
|
||||
|
||||
// Copy data from our buffer list into the input buffer
|
||||
buf.put(decodeUnitData, 0, decodeUnitLength);
|
||||
nextInputBuffer.put(decodeUnitData, 0, decodeUnitLength);
|
||||
|
||||
if (!queueInputBuffer(inputBufferIndex,
|
||||
0, buf.position(),
|
||||
timestampUs, codecFlags)) {
|
||||
if (!queueNextInputBuffer(timestampUs, codecFlags)) {
|
||||
return MoonBridge.DR_NEED_IDR;
|
||||
}
|
||||
|
||||
@@ -1176,18 +1461,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
}
|
||||
|
||||
private boolean replaySps() {
|
||||
int inputIndex = dequeueInputBuffer();
|
||||
if (inputIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
|
||||
if (inputBuffer == null) {
|
||||
if (!fetchNextInputBuffer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write the Annex B header
|
||||
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||
nextInputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||
|
||||
// Switch the H264 profile back to high
|
||||
savedSps.profileIdc = 100;
|
||||
@@ -1198,15 +1477,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
// The H264Utils.writeSPS function safely handles
|
||||
// Annex B NALUs (including NALUs with escape sequences)
|
||||
ByteBuffer escapedNalu = H264Utils.writeSPS(savedSps, 128);
|
||||
inputBuffer.put(escapedNalu);
|
||||
nextInputBuffer.put(escapedNalu);
|
||||
|
||||
// No need for the SPS anymore
|
||||
savedSps = null;
|
||||
|
||||
// Queue the new SPS
|
||||
return queueInputBuffer(inputIndex,
|
||||
0, inputBuffer.position(),
|
||||
0, MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
||||
return queueNextInputBuffer(0, MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1269,18 +1546,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
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) {
|
||||
@@ -1305,7 +1578,6 @@ 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";
|
||||
@@ -1338,11 +1610,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
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 += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
||||
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 += "Vendor params: ";
|
||||
List<String> params = renderer.videoDecoder.getSupportedVendorParameters();
|
||||
if (params.isEmpty()) {
|
||||
str += "NONE";
|
||||
@@ -1352,7 +1624,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
str += param + " ";
|
||||
}
|
||||
}
|
||||
str += "\n";
|
||||
str += "\n";*/
|
||||
}
|
||||
str += "Foreground: "+renderer.foreground+"\n";
|
||||
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
||||
@@ -1371,18 +1643,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
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";
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (originalException instanceof CodecException) {
|
||||
CodecException ce = (CodecException) originalException;
|
||||
@@ -1397,20 +1657,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -44,7 +44,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;
|
||||
@@ -82,18 +83,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:
|
||||
@@ -449,13 +450,17 @@ public class MediaCodecHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (tryNumber < 2) {
|
||||
if (tryNumber < 2 &&
|
||||
(!Build.MANUFACTURER.equalsIgnoreCase("xiaomi") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M)) {
|
||||
// MediaTek decoders don't use vendor-defined keys for low latency mode. Instead, they have a modified
|
||||
// version of AOSP's ACodec.cpp which supports the "vdec-lowlatency" option. This option is passed down
|
||||
// to the decoder as OMX.MTK.index.param.video.LowLatencyDecode.
|
||||
//
|
||||
// This option is also plumbed for Amazon Amlogic-based devices like the Fire TV 3. Not only does it
|
||||
// reduce latency on Amlogic, it fixes the HEVC bug that causes the decoder to not output any frames.
|
||||
// Unfortunately, it does the exact opposite for the Xiaomi MITV4-ANSM0, breaking it in the way that
|
||||
// Fire TV was broken prior to vdec-lowlatency :(
|
||||
//
|
||||
// On Fire TV 3, vdec-lowlatency is translated to OMX.amazon.fireos.index.video.lowLatencyDecode.
|
||||
//
|
||||
// https://github.com/yuan1617/Framwork/blob/master/frameworks/av/media/libstagefright/ACodec.cpp
|
||||
@@ -717,7 +722,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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
@@ -33,12 +35,20 @@ public class NvConnection {
|
||||
private ConnectionContext context;
|
||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||
private final boolean isMonkey;
|
||||
|
||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
||||
private final boolean batchMouseInput;
|
||||
|
||||
private static final int MOUSE_BATCH_PERIOD_MS = 5;
|
||||
private Timer mouseInputTimer;
|
||||
private final Object mouseInputLock = new Object();
|
||||
private short relMouseX, relMouseY, relMouseWidth, relMouseHeight;
|
||||
private short absMouseX, absMouseY, absMouseWidth, absMouseHeight;
|
||||
|
||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert, boolean batchMouseInput)
|
||||
{
|
||||
this.host = host;
|
||||
this.cryptoProvider = cryptoProvider;
|
||||
this.uniqueId = uniqueId;
|
||||
this.batchMouseInput = batchMouseInput;
|
||||
|
||||
this.context = new ConnectionContext();
|
||||
this.context.streamConfig = config;
|
||||
@@ -70,6 +80,11 @@ public class NvConnection {
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// Stop sending additional input
|
||||
if (mouseInputTimer != null) {
|
||||
mouseInputTimer.cancel();
|
||||
}
|
||||
|
||||
// Interrupt any pending connection. This is thread-safe.
|
||||
MoonBridge.interruptConnection();
|
||||
|
||||
@@ -83,6 +98,24 @@ public class NvConnection {
|
||||
// Now a pending connection can be processed
|
||||
connectionAllowed.release();
|
||||
}
|
||||
|
||||
private void flushMousePosition() {
|
||||
synchronized (mouseInputLock) {
|
||||
if (relMouseX != 0 || relMouseY != 0) {
|
||||
if (relMouseWidth != 0 || relMouseHeight != 0) {
|
||||
MoonBridge.sendMouseMoveAsMousePosition(relMouseX, relMouseY, relMouseWidth, relMouseHeight);
|
||||
}
|
||||
else {
|
||||
MoonBridge.sendMouseMove(relMouseX, relMouseY);
|
||||
}
|
||||
relMouseX = relMouseY = relMouseWidth = relMouseHeight = 0;
|
||||
}
|
||||
if (absMouseX != 0 || absMouseY != 0 || absMouseWidth != 0 || absMouseHeight != 0) {
|
||||
MoonBridge.sendMousePosition(absMouseX, absMouseY, absMouseWidth, absMouseHeight);
|
||||
absMouseX = absMouseY = absMouseWidth = absMouseHeight = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean startApp() throws XmlPullParserException, IOException
|
||||
{
|
||||
@@ -292,6 +325,21 @@ public class NvConnection {
|
||||
// to stop the connection themselves. We need to release their
|
||||
// semaphore count for them.
|
||||
connectionAllowed.release();
|
||||
return;
|
||||
}
|
||||
|
||||
if (batchMouseInput) {
|
||||
// High polling rate mice can cause GeForce Experience's input queue to get backed up,
|
||||
// causing massive input latency. We counter this by limiting our mouse events to 200 Hz
|
||||
// which appears to avoid triggering the issue on all known configurations.
|
||||
mouseInputTimer = new Timer("MouseInput", true);
|
||||
mouseInputTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Flush the mouse position every 5 ms
|
||||
flushMousePosition();
|
||||
}
|
||||
}, MOUSE_BATCH_PERIOD_MS, MOUSE_BATCH_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,27 +349,65 @@ public class NvConnection {
|
||||
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseMove(deltaX, deltaY);
|
||||
synchronized (mouseInputLock) {
|
||||
relMouseX += deltaX;
|
||||
relMouseY += deltaY;
|
||||
|
||||
// Reset these to ensure we don't send this as a position update
|
||||
relMouseWidth = 0;
|
||||
relMouseHeight = 0;
|
||||
}
|
||||
|
||||
if (!batchMouseInput) {
|
||||
flushMousePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
|
||||
synchronized (mouseInputLock) {
|
||||
absMouseX = x;
|
||||
absMouseY = y;
|
||||
absMouseWidth = referenceWidth;
|
||||
absMouseHeight = referenceHeight;
|
||||
}
|
||||
|
||||
if (!batchMouseInput) {
|
||||
flushMousePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseMoveAsMousePosition(short deltaX, short deltaY, short referenceWidth, short referenceHeight)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseMoveAsMousePosition(deltaX, deltaY, referenceWidth, referenceHeight);
|
||||
synchronized (mouseInputLock) {
|
||||
// Only accumulate the delta if the reference size is the same
|
||||
if (relMouseWidth == referenceWidth && relMouseHeight == referenceHeight) {
|
||||
relMouseX += deltaX;
|
||||
relMouseY += deltaY;
|
||||
}
|
||||
else {
|
||||
relMouseX = deltaX;
|
||||
relMouseY = deltaY;
|
||||
}
|
||||
|
||||
relMouseWidth = referenceWidth;
|
||||
relMouseHeight = referenceHeight;
|
||||
}
|
||||
|
||||
if (!batchMouseInput) {
|
||||
flushMousePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonDown(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
flushMousePosition();
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
@@ -329,6 +415,7 @@ public class NvConnection {
|
||||
public void sendMouseButtonUp(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
flushMousePosition();
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
@@ -364,12 +451,14 @@ public class NvConnection {
|
||||
|
||||
public void sendMouseScroll(final byte scrollClicks) {
|
||||
if (!isMonkey) {
|
||||
flushMousePosition();
|
||||
MoonBridge.sendMouseScroll(scrollClicks);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseHighResScroll(final short scrollAmount) {
|
||||
if (!isMonkey) {
|
||||
flushMousePosition();
|
||||
MoonBridge.sendMouseHighResScroll(scrollAmount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.Display;
|
||||
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
@@ -46,6 +47,7 @@ public class PreferenceConfiguration {
|
||||
private static final String FRAME_PACING_PREF_STRING = "frame_pacing";
|
||||
private static final String ABSOLUTE_MOUSE_MODE_PREF_STRING = "checkbox_absolute_mouse_mode";
|
||||
private static final String ENABLE_AUDIO_FX_PREF_STRING = "checkbox_enable_audiofx";
|
||||
private static final String REDUCE_REFRESH_RATE_PREF_STRING = "checkbox_reduce_refresh_rate";
|
||||
|
||||
static final String DEFAULT_RESOLUTION = "1280x720";
|
||||
static final String DEFAULT_FPS = "60";
|
||||
@@ -77,6 +79,7 @@ public class PreferenceConfiguration {
|
||||
private static final String DEFAULT_FRAME_PACING = "latency";
|
||||
private static final boolean DEFAULT_ABSOLUTE_MOUSE_MODE = false;
|
||||
private static final boolean DEFAULT_ENABLE_AUDIO_FX = false;
|
||||
private static final boolean DEFAULT_REDUCE_REFRESH_RATE = false;
|
||||
|
||||
public static final int FORCE_H265_ON = -1;
|
||||
public static final int AUTOSELECT_H265 = 0;
|
||||
@@ -120,6 +123,7 @@ public class PreferenceConfiguration {
|
||||
public int framePacing;
|
||||
public boolean absoluteMouseMode;
|
||||
public boolean enableAudioFx;
|
||||
public boolean reduceRefreshRate;
|
||||
|
||||
public static boolean isNativeResolution(int width, int height) {
|
||||
// It's not a native resolution if it matches an existing resolution option
|
||||
@@ -145,6 +149,31 @@ public class PreferenceConfiguration {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have a screen that has semi-square dimensions, we may want to change our behavior
|
||||
// to allow any orientation and vertical+horizontal resolutions.
|
||||
public static boolean isSquarishScreen(int width, int height) {
|
||||
float longDim = Math.max(width, height);
|
||||
float shortDim = Math.min(width, height);
|
||||
|
||||
// We just put the arbitrary cutoff for a square-ish screen at 1.3
|
||||
return longDim / shortDim < 1.3f;
|
||||
}
|
||||
|
||||
public static boolean isSquarishScreen(Display display) {
|
||||
int width, height;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
width = display.getMode().getPhysicalWidth();
|
||||
height = display.getMode().getPhysicalHeight();
|
||||
}
|
||||
else {
|
||||
width = display.getWidth();
|
||||
height = display.getHeight();
|
||||
}
|
||||
|
||||
return isSquarishScreen(width, height);
|
||||
}
|
||||
|
||||
private static String convertFromLegacyResolutionString(String resString) {
|
||||
if (resString.equalsIgnoreCase("360p")) {
|
||||
return RES_360P;
|
||||
@@ -475,6 +504,7 @@ public class PreferenceConfiguration {
|
||||
config.enableLatencyToast = prefs.getBoolean(LATENCY_TOAST_PREF_STRING, DEFAULT_LATENCY_TOAST);
|
||||
config.absoluteMouseMode = prefs.getBoolean(ABSOLUTE_MOUSE_MODE_PREF_STRING, DEFAULT_ABSOLUTE_MOUSE_MODE);
|
||||
config.enableAudioFx = prefs.getBoolean(ENABLE_AUDIO_FX_PREF_STRING, DEFAULT_ENABLE_AUDIO_FX);
|
||||
config.reduceRefreshRate = prefs.getBoolean(REDUCE_REFRESH_RATE_PREF_STRING, DEFAULT_REDUCE_REFRESH_RATE);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -38,11 +39,16 @@ import java.util.Arrays;
|
||||
|
||||
public class StreamSettings extends Activity {
|
||||
private PreferenceConfiguration previousPrefs;
|
||||
private int previousDisplayPixelCount;
|
||||
|
||||
// HACK for Android 9
|
||||
static DisplayCutout displayCutoutP;
|
||||
|
||||
void reloadSettings() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display.Mode mode = getWindowManager().getDefaultDisplay().getMode();
|
||||
previousDisplayPixelCount = mode.getPhysicalWidth() * mode.getPhysicalHeight();
|
||||
}
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commitAllowingStateLoss();
|
||||
@@ -79,6 +85,24 @@ public class StreamSettings extends Activity {
|
||||
reloadSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display.Mode mode = getWindowManager().getDefaultDisplay().getMode();
|
||||
|
||||
// If the display's physical pixel count has changed, we consider that it's a new display
|
||||
// and we should reload our settings (which include display-dependent values).
|
||||
//
|
||||
// NB: We aren't using displayId here because that stays the same (DEFAULT_DISPLAY) when
|
||||
// switching between screens on a foldable device.
|
||||
if (mode.getPhysicalWidth() * mode.getPhysicalHeight() != previousDisplayPixelCount) {
|
||||
reloadSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
// NOTE: This will NOT be called on Android 13+ with android:enableOnBackInvokedCallback="true"
|
||||
public void onBackPressed() {
|
||||
@@ -106,7 +130,7 @@ public class StreamSettings extends Activity {
|
||||
pref.setValue(value);
|
||||
}
|
||||
|
||||
private void addNativeResolutionEntry(int nativeWidth, int nativeHeight, boolean insetsRemoved) {
|
||||
private void addNativeResolutionEntry(int nativeWidth, int nativeHeight, boolean insetsRemoved, boolean portrait) {
|
||||
ListPreference pref = (ListPreference) findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING);
|
||||
|
||||
String newName;
|
||||
@@ -118,6 +142,15 @@ public class StreamSettings extends Activity {
|
||||
newName = getResources().getString(R.string.resolution_prefix_native);
|
||||
}
|
||||
|
||||
if (PreferenceConfiguration.isSquarishScreen(nativeWidth, nativeHeight)) {
|
||||
if (portrait) {
|
||||
newName += " " + getResources().getString(R.string.resolution_prefix_native_portrait);
|
||||
}
|
||||
else {
|
||||
newName += " " + getResources().getString(R.string.resolution_prefix_native_landscape);
|
||||
}
|
||||
}
|
||||
|
||||
newName += " ("+nativeWidth+"x"+nativeHeight+")";
|
||||
|
||||
String newValue = nativeWidth+"x"+nativeHeight;
|
||||
@@ -147,6 +180,13 @@ public class StreamSettings extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private void addNativeResolutionEntries(int nativeWidth, int nativeHeight, boolean insetsRemoved) {
|
||||
if (PreferenceConfiguration.isSquarishScreen(nativeWidth, nativeHeight)) {
|
||||
addNativeResolutionEntry(nativeHeight, nativeWidth, insetsRemoved, true);
|
||||
}
|
||||
addNativeResolutionEntry(nativeWidth, nativeHeight, insetsRemoved, false);
|
||||
}
|
||||
|
||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||
int matchingCount = 0;
|
||||
|
||||
@@ -300,7 +340,7 @@ public class StreamSettings extends Activity {
|
||||
int width = Math.max(metrics.widthPixels - widthInsets, metrics.heightPixels - heightInsets);
|
||||
int height = Math.min(metrics.widthPixels - widthInsets, metrics.heightPixels - heightInsets);
|
||||
|
||||
addNativeResolutionEntry(width, height, false);
|
||||
addNativeResolutionEntries(width, height, false);
|
||||
hasInsets = true;
|
||||
}
|
||||
}
|
||||
@@ -325,7 +365,7 @@ public class StreamSettings extends Activity {
|
||||
// unless they report greater than 4K resolutions.
|
||||
if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION) ||
|
||||
(width > 3840 || height > 2160)) {
|
||||
addNativeResolutionEntry(width, height, hasInsets);
|
||||
addNativeResolutionEntries(width, height, hasInsets);
|
||||
}
|
||||
|
||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||
@@ -435,7 +475,7 @@ public class StreamSettings extends Activity {
|
||||
getActivity().getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
|
||||
int width = Math.max(metrics.widthPixels, metrics.heightPixels);
|
||||
int height = Math.min(metrics.widthPixels, metrics.heightPixels);
|
||||
addNativeResolutionEntry(width, height, false);
|
||||
addNativeResolutionEntries(width, height, false);
|
||||
}
|
||||
else {
|
||||
// On Android 4.1, we have to resort to reflection to invoke hidden APIs
|
||||
@@ -446,7 +486,7 @@ public class StreamSettings extends Activity {
|
||||
Method getRawWidthFunc = Display.class.getMethod("getRawWidth");
|
||||
int width = (Integer) getRawWidthFunc.invoke(display);
|
||||
int height = (Integer) getRawHeightFunc.invoke(display);
|
||||
addNativeResolutionEntry(Math.max(width, height), Math.min(width, height), false);
|
||||
addNativeResolutionEntries(Math.max(width, height), Math.min(width, height), false);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Submodule app/src/main/jni/moonlight-core/moonlight-common-c updated: d247873ade...54825845e7
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M21,2L3,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h7v2L8,20v2h8v-2h-2v-2h7c1.1,0 2,-0.9 2,-2L23,4c0,-1.1 -0.9,-2 -2,-2zM21,16L3,16L3,4h18v12z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M21,2L3,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h7v2L8,20v2h8v-2h-2v-2h7c1.1,0 2,-0.9 2,-2L23,4c0,-1.1 -0.9,-2 -2,-2zM21,16L3,16L3,4h18v12z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<vector android:height="128dp" android:width="128dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM9.5,16.5v-9l7,4.5L9.5,16.5z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" />
|
||||
</vector>
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
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"
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
<string name="pcview_menu_header_offline">Offline</string>
|
||||
<string name="pcview_menu_header_online">Online</string>
|
||||
<!-- Array strings -->
|
||||
<string name="videoformat_hevcauto">Verwende HEVC so fern stabile Unterstützung vorhanden ist</string>
|
||||
<string name="videoformat_hevcauto">Automatisch</string>
|
||||
<string name="videoformat_hevcalways">Immer HEVC verwenden (könnte Crashes verursachen)</string>
|
||||
<string name="videoformat_hevcnever">Nie HEVC verwenden</string>
|
||||
<string name="title_frame_pacing">Video Frame-Pacing</string>
|
||||
@@ -242,4 +242,21 @@
|
||||
<string name="resolution_720p">720p</string>
|
||||
<string name="fps_60">60 FPS</string>
|
||||
<string name="fps_90">90 FPS</string>
|
||||
<string name="summary_troubleshooting">Tipps zur Diagnose und Behebung häufiger Streaming-Probleme anzeigen</string>
|
||||
<string name="pacing_balanced_alt">Ausbalanciert mit FPS-Limit</string>
|
||||
<string name="summary_checkbox_absolute_mouse_mode">Dies kann die Mausbeschleunigung für die Fernsteuerung natürlicher gestalten, ist aber mit vielen Spielen nicht kompatibel.</string>
|
||||
<string name="title_troubleshooting">Anleitung zur Fehlersuche</string>
|
||||
<string name="title_privacy_policy">Datenschutzbestimmungen</string>
|
||||
<string name="title_checkbox_enable_audiofx">Unterstützung des System-Equalizer aktivieren</string>
|
||||
<string name="summary_setup_guide">Zeige Anweisungen zum Einrichten Ihres Gaming-PCs für das Streaming an</string>
|
||||
<string name="title_checkbox_absolute_mouse_mode">Modus für ferngesteuerte Maus</string>
|
||||
<string name="category_help">Hilfe</string>
|
||||
<string name="title_setup_guide">Einstellungsanleitung</string>
|
||||
<string name="summary_privacy_policy">Moonlight\'s Datenschutzbestimmungen anzeigen</string>
|
||||
<string name="summary_seekbar_deadzone">Hinweis: Einige Spiele können eine größere Controller-Totzone erzwingen als die, die in Moonlight konfiguriert ist.</string>
|
||||
<string name="summary_checkbox_enable_audiofx">Ermöglicht das Funktionieren von Audioeffekten beim Streaming, kann aber die Audio-Latenz erhöhen</string>
|
||||
<string name="resolution_prefix_native_portrait">(Portrait)</string>
|
||||
<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>
|
||||
</resources>
|
||||
@@ -48,7 +48,9 @@
|
||||
<string name="conn_terminated_msg">La conexión ha finalizando</string>
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">Dirección IP del PC con GeForce</string>
|
||||
<string name="searching_pc">Buscando por PCs con GeForce Experience ejecutándose…</string>
|
||||
<string name="searching_pc">Buscando por PCs con GameStream ejecutándose...
|
||||
\n
|
||||
\nVerifica que GameStream esté activado en las opciones de SHIELD dentro de GeForce Experience.</string>
|
||||
<string name="yes">Si</string>
|
||||
<string name="no">No</string>
|
||||
<string name="lost_connection">Conexión perdida</string>
|
||||
@@ -75,7 +77,7 @@
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Configuración básica</string>
|
||||
<string name="title_resolution_list">Seleccionar resolución y FPS</string>
|
||||
<string name="summary_resolution_list">Establecer unos valores demasiado altos puede causar lag o cierres inesperados</string>
|
||||
<string name="summary_resolution_list">Establecer unos valores demasiado altos puede causar retraso o cierres inesperados.</string>
|
||||
<string name="title_seekbar_bitrate">Seleccionar bitrate de vídeo</string>
|
||||
<string name="summary_seekbar_bitrate">Usa bitrate bajo para reducir "parpadeo". Incrementa el bitrate para mayor calidad de imagen.</string>
|
||||
<string name="title_checkbox_stretch_video">Ajustar vídeo a pantalla completa</string>
|
||||
@@ -87,7 +89,7 @@
|
||||
<string name="title_seekbar_deadzone">Ajustar zona muerta del stick analógico</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Driver para mando Xbox 360/One</string>
|
||||
<string name="summary_checkbox_xb1_driver">Activa el driver USB incluido para dispositivos sin soporte nativo para mandos de Xbox.</string>
|
||||
<string name="summary_checkbox_xb1_driver">Activa el driver USB incluido para dispositivos sin soporte nativo para mandos de Xbox</string>
|
||||
<string name="category_on_screen_controls_settings">Configuración de controles en pantalla</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">Mostrar controles en pantalla</string>
|
||||
<string name="summary_checkbox_show_onscreen_controls">Muestra controles virtuales superpuestos en la pantalla táctil</string>
|
||||
@@ -105,17 +107,17 @@
|
||||
<string name="summary_checkbox_host_audio">Reproducir audio en el ordenador y en este dispositivo</string>
|
||||
<string name="category_advanced_settings">Configuración avanzada</string>
|
||||
<string name="title_video_format">Cambiar configuración HEVC</string>
|
||||
<string name="summary_video_format">HEVC reduce el ancho de banda de vídeo, pero requiere un dispositivo bastante actual.</string>
|
||||
<string name="summary_video_format">HEVC reduce el ancho de banda de vídeo, pero requiere un dispositivo reciente</string>
|
||||
<!-- Array strings -->
|
||||
<string name="videoformat_hevcauto">Usar HEVC solo si es estable</string>
|
||||
<string name="videoformat_hevcauto">Usar HEVC sólo si es estable</string>
|
||||
<string name="videoformat_hevcalways">Siempre usar HEVC (puede fallar)</string>
|
||||
<string name="videoformat_hevcnever">Nunca usar HEVC</string>
|
||||
<string name="nettest_title_done">Prueba de Red Completada</string>
|
||||
<string name="scut_not_paired">PC no emparejado</string>
|
||||
<string name="scut_invalid_uuid">PC proporcionado no es válido</string>
|
||||
<string name="scut_invalid_app_id">App proporcionada no es válida</string>
|
||||
<string name="help_loading_title">Visor de Ayuda</string>
|
||||
<string name="pcview_menu_header_online">En Línea</string>
|
||||
<string name="scut_invalid_uuid">El PC proporcionado no es válido</string>
|
||||
<string name="scut_invalid_app_id">La App proporcionada no es válida</string>
|
||||
<string name="help_loading_title">Visor de ayuda</string>
|
||||
<string name="pcview_menu_header_online">En línea</string>
|
||||
<string name="pcview_menu_header_unknown">Actualizando</string>
|
||||
<string name="pcview_menu_test_network">Probar Conexión de Red</string>
|
||||
<string name="pcview_menu_details">Ver Detalles</string>
|
||||
@@ -131,11 +133,127 @@
|
||||
<string name="title_decoding_error">Falló el Decodificador de Video</string>
|
||||
<string name="scut_deleted_pc">PC eliminado</string>
|
||||
<string name="scut_pc_not_found">PC no encontrado</string>
|
||||
<string name="help_loading_msg">Cargando página de ayuda …</string>
|
||||
<string name="help_loading_msg">Cargando página de ayuda…</string>
|
||||
<string name="pcview_menu_header_offline">Fuera de línea</string>
|
||||
<string name="nettest_text_success">Tu red parece no estar bloqueando Moonlight. Si aun tienes problemas de conexión, comprueba la configuración del firewall de tu PC.
|
||||
\n
|
||||
\nSi estas intentado transmitir mediante Internet, instala en tu pc la Moonlight Internet Hosting Tool y ejecuta el Internet Streaming Tester incluido para comprobar la conexión de tu PC a internet.</string>
|
||||
<string name="nettest_text_blocked">La conexión de red actual de tu dispositivo esta bloqueando Moonlight. La Transmisión mediante Internet puede no funcionar mientras estés conectado a esta red.</string>
|
||||
<string name="pair_already_in_progress">Emparejamiento ya en progreso</string>
|
||||
<string name="resolution_prefix_native_fullscreen">Pantalla Completa Nativa</string>
|
||||
<string name="title_audio_config_list">Configuración de Sonido Envolvente</string>
|
||||
<string name="title_decoding_reset">Reiniciar Opciones de Video</string>
|
||||
<string name="no_video_received_error">No se recibe video desde el equipo anfitrión.</string>
|
||||
<string name="applist_details_id">ID de Aplicación:</string>
|
||||
<string name="title_checkbox_mouse_emulation">Emulación de ratón usando el mando</string>
|
||||
<string name="pacing_latency">Preferir la menor latencia posible</string>
|
||||
<string name="pacing_balanced">Equilibrado</string>
|
||||
<string name="pacing_smoothness">Preferir mayor fluidez de video (puede incrementar significativamente la latencia)</string>
|
||||
<string name="perf_overlay_decoder">Decodificador: %1$s</string>
|
||||
<string name="perf_overlay_incomingfps">Tasa de cuadros obtenida de la red: %1$.2f FPS</string>
|
||||
<string name="perf_overlay_renderingfps">Tasa de Cuadros Renderizada: %1$.2f FPS</string>
|
||||
<string name="perf_overlay_netdrops">Cuadros perdidos por tu conexión de internet: %1$.2f%%</string>
|
||||
<string name="perf_overlay_netlatency">Latencia promedio de red:%1$d ms (variación: %2$d ms)</string>
|
||||
<string name="perf_overlay_dectime">Tiempo promedio de descodificado: %1$.2f ms</string>
|
||||
<string name="perf_overlay_streamdetails">Transmisión video: %1$s %2$.2f FPS</string>
|
||||
<string name="title_fps_list">Tasa de cuadros en el video</string>
|
||||
<string name="summary_fps_list">Incrementar para una experiencia mas fluida en la transmisión. Disminuir para una mejoría en el rendimiento en dispositivos de gama de entrada.</string>
|
||||
<string name="summary_reset_osc">Reiniciar toda la configuración a las predeterminadas de tamaño y posición para los controles en pantalla</string>
|
||||
<string name="summary_troubleshooting">Mostrar consejos para diagnosticar y corregir errores comunes respecto a la transmisión</string>
|
||||
<string name="title_privacy_policy">Política de Privacidad</string>
|
||||
<string name="resolution_360p">360p</string>
|
||||
<string name="resolution_1440p">1440p</string>
|
||||
<string name="resolution_4k">4K</string>
|
||||
<string name="suffix_osc_opacity">%</string>
|
||||
<string name="title_checkbox_enable_pip">Actividad modo Pantalla-en-Pantalla para observador</string>
|
||||
<string name="resolution_720p">720p</string>
|
||||
<string name="fps_120">120 FPS</string>
|
||||
<string name="audioconf_71surround">7.1 Sonido Envolvente</string>
|
||||
<string name="title_troubleshooting">Guía de Solución de Errores</string>
|
||||
<string name="resolution_1080p">1080p</string>
|
||||
<string name="summary_frame_pacing">Especificar como balancear la latencia de video y la fluidez</string>
|
||||
<string name="pacing_balanced_alt">Equilibrado con limitador de FPS (Cuadros por Segundo)</string>
|
||||
<string name="audioconf_51surround">5.1 Sonido Envolvente</string>
|
||||
<string name="audioconf_stereo">Estéreo</string>
|
||||
<string name="check_ports_msg">Revisa la configuración de tu firewall, así como, las reglas de redireccionamiento para él(los) puerto(s):</string>
|
||||
<string name="error_usb_prohibited">El acceso a USB está restringido en tu dispositivo por el administrador. Revisar tus ajustes en MDM o Knox.</string>
|
||||
<string name="message_decoding_error">Moonlight se ha cerrado debido a una incompatibilidad con el decodificador de vídeo de este dispositivo. Asegúrate de que GeForce Experience está actualizado a la última versión en tu PC. Intenta ajustar la configuración de streaming si los bloqueos continúan.</string>
|
||||
<string name="message_decoding_reset">La decodificación de video de tu equipo sigue fallando con las opciones elegidas. Las opciones de la transmisión han sido revertidas a las predeterminadas.</string>
|
||||
<string name="video_decoder_init_failed">El descodificador de video no pudo ser iniciado. Tu dispositivo puede no soportar la resolución elegida o la tasa de cuadros por segundo.</string>
|
||||
<string name="no_frame_received_error">La conexión de internet no está teniendo un buen desempeño. Reduce la tasa de bits de video o prueba en una conexión de mayor velocidad.</string>
|
||||
<string name="unable_to_pin_shortcut">Tu lanzador predeterminado no permite la creación de accesos directos anclados.</string>
|
||||
<string name="early_termination_error">Algo ha salido mal en el PC anfitrión cuando se inicio la transmisión.
|
||||
\n
|
||||
\nVerifica que no tengas ningún contenido protegido por DRM abierto en el equipo. También prueba reiniciando el equipo anfitrión.
|
||||
\n
|
||||
\nSi el problema persiste, intenta reinstalar los controladores de la tarjeta de video (GPU) además de GeForce Experience.</string>
|
||||
<string name="title_details">Detalles</string>
|
||||
<string name="poor_connection_msg">Conexión pobre al PC</string>
|
||||
<string name="applist_menu_details">Ver Detalles</string>
|
||||
<string name="applist_menu_scut">Crear Acceso Directo</string>
|
||||
<string name="applist_menu_tv_channel">Agregar al Canal</string>
|
||||
<string name="applist_menu_hide_app">Esconder Aplicación</string>
|
||||
<string name="applist_connect_msg">Conectando al Equipo…</string>
|
||||
<string name="summary_checkbox_mouse_nav_buttons">Activar esta opción puede desconfigurar la función de clic derecho en algunos dispositivos inestables</string>
|
||||
<string name="text_native_res_dialog">Modos de resolución nativa no son oficialmente soportados por GeForce Experience, así que no se no se aplicarán por su cuenta en la pantalla del equipo anfitrión. Vas a necesitar ajustarlos manualmente en el juego.
|
||||
\n
|
||||
\nSi eliges crear una resolución personalizada en el Panel de Control de Nvidia para ajustarse a la resolución de tu dispositivo, por favor considera haber leído y entendido la advertencia de NVIDIA respectivo a cualquier daño posible a tu monitor, inestabilidad del equipo, y otros problemas potencialmente posibles.
|
||||
\n
|
||||
\nNo somos responsables de cualquier problema por haber creador una resolución personalizada en tu PC.
|
||||
\n
|
||||
\nFinalmente, tu dispositivo o tu Pc anfitrión, podrían no soportar la transmisión a resolución nativa. Si no funciona en tu equipo, puede que solo no tengas suerte de momento.</string>
|
||||
<string name="resolution_prefix_native">Nativo</string>
|
||||
<string name="summary_audio_config_list">Activar sonido envolvente de 5.1 o 7.1 canales para sistemas de teatro en casa</string>
|
||||
<string name="title_checkbox_vibrate_fallback">Emular la vibración del mando con vibración del equipo</string>
|
||||
<string name="summary_checkbox_enable_audiofx">Permitir funcionar efectos de sonido durante la transmisión, pero podría incrementar la latencia del sonido</string>
|
||||
<string name="summary_checkbox_usb_bind_all">Usar el controlador de USB de Moonlight para todos los mandos soportados, incluso si el control de Xbox se encuentra presente</string>
|
||||
<string name="summary_checkbox_mouse_emulation">Presionar por un periodo largo el botón de inicio hará cambiar el mando a modo de ratón</string>
|
||||
<string name="title_checkbox_mouse_nav_buttons">Activar los botones atrás y adelante del ratón</string>
|
||||
<string name="title_unlock_fps">Desbloquear todas las opciones de tasa de cuadros disponibles</string>
|
||||
<string name="summary_unlock_fps">Transmitir a 90 o 120 FPS puede reducir latencia en dispositivos de gama alta pero puede causar retrasos en dispositivos que no lo soporten</string>
|
||||
<string name="title_enable_perf_overlay">Mostrar estadísticas de rendimiento mientras la transmisión está activa</string>
|
||||
<string name="summary_enable_post_stream_toast">Mostrar un mensaje sobre la información de latencia cuando la transmisión termine</string>
|
||||
<string name="help">Ayuda</string>
|
||||
<string name="delete_pc_msg">¿Estás seguro de eliminar este PC\?</string>
|
||||
<string name="slow_connection_msg">Conexión lenta al PC
|
||||
\nReduce la tasa de bits</string>
|
||||
<string name="addpc_wrong_sitelocal">La dirección no luce correcta. Debes usar la dirección IP Pública de tu Rúter para la transmisión por Internet.</string>
|
||||
<string name="title_native_res_dialog">Advertencia Resolución Nativa</string>
|
||||
<string name="suffix_seekbar_bitrate_mbps">Mbps</string>
|
||||
<string name="category_input_settings">Opciones de Entrada</string>
|
||||
<string name="title_checkbox_touchscreen_trackpad">Usa la pantalla táctil como panel táctil</string>
|
||||
<string name="summary_checkbox_touchscreen_trackpad">Si está activo, la pantalla táctil funcionará como panel táctil. Si está desactivado, la pantalla táctil directamente controlará el cursor del ratón.</string>
|
||||
<string name="summary_seekbar_deadzone">Nota: Algunos juegos pueden forzar una zona muerta mayor a la que Moonlight esta configurada a usar.</string>
|
||||
<string name="title_checkbox_flip_face_buttons">Invertir botones frontales</string>
|
||||
<string name="summary_checkbox_absolute_mouse_mode">Esto puede hacer que la aceleración de mouse se comporte mas natural para uso de escritorio remoto, pero es incompatible con muchos juegos.</string>
|
||||
<string name="title_checkbox_enable_audiofx">Activar el soporte para ecualizador de sistema</string>
|
||||
<string name="summary_checkbox_vibrate_fallback">Vibrar el dispositivo para emular el retumbe si tu mando de control no lo soporta</string>
|
||||
<string name="title_checkbox_usb_bind_all">Sobrescribir el soporte nativo del control de Xbox</string>
|
||||
<string name="summary_checkbox_flip_face_buttons">Invertir botones frontales A/B y X/Y para los mandos y los botones sobre la pantalla</string>
|
||||
<string name="title_checkbox_absolute_mouse_mode">Modo de ratón para escritorio remoto</string>
|
||||
<string name="title_checkbox_vibrate_osc">Activar vibración</string>
|
||||
<string name="summary_checkbox_vibrate_osc">Hacer vibrar tu dispositivo para emular la vibración del juego para los controles en pantalla</string>
|
||||
<string name="title_reset_osc">Borrar la configuración guardada de la plantilla para el control en pantalla</string>
|
||||
<string name="dialog_title_reset_osc">Reiniciar a Plantilla Predeterminada</string>
|
||||
<string name="dialog_text_reset_osc">¿Estás seguro de que quieres eliminar tus configuraciones guardadas para las plantillas de los controles en pantalla\?</string>
|
||||
<string name="toast_reset_osc_success">Establecer Controles en Pantalla en modo Predeterminado</string>
|
||||
<string name="title_osc_opacity">Cambiar la transparencia de los controles en pantalla</string>
|
||||
<string name="summary_osc_opacity">Hacer los controles en pantalla más/menos transparentes</string>
|
||||
<string name="dialog_title_osc_opacity">Cambiar opacidad</string>
|
||||
<string name="summary_checkbox_enable_pip">Permite que la transmisión sea vista (pero no controlable) mientras se usa la multitarea</string>
|
||||
<string name="title_disable_frame_drop">Nunca disminuir cuadros</string>
|
||||
<string name="summary_disable_frame_drop">Puede reducir micro-tartamudeos (Stuttering) en algunos dispositivos , pero puede incrementar latencia</string>
|
||||
<string name="title_enable_hdr">Activar HDR (Alto Rango Dinámico / Experimental)</string>
|
||||
<string name="summary_enable_hdr">Transmitir HDR (Alto Rango Dinámico) cuando el juego, el PC y la Tarjeta de Video (GPU) lo soporten. HDR requiere de una Tarjeta de Video de la serie GTX 1000 o superior.</string>
|
||||
<string name="summary_enable_perf_overlay">Mostrar en tiempo real la información del desempeño de la transmisión mientras está activa la misma</string>
|
||||
<string name="title_enable_post_stream_toast">Mostrar mensajes sobre latencia mientras se transmite</string>
|
||||
<string name="category_help">Ayuda</string>
|
||||
<string name="title_setup_guide">Guía para la Configuración</string>
|
||||
<string name="summary_setup_guide">Mostrar instrucciones para como configurar tu PC para juegos para la transmisión</string>
|
||||
<string name="summary_privacy_policy">Mostrar la política de privacidad de Moonlight</string>
|
||||
<string name="resolution_480p">480p</string>
|
||||
<string name="fps_30">30 FPS</string>
|
||||
<string name="fps_60">60 FPS</string>
|
||||
<string name="fps_90">90 FPS</string>
|
||||
<string name="title_frame_pacing">Ritmo de cuadros por segundo en video</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -254,4 +254,5 @@
|
||||
<string name="audioconf_71surround">7.1 서라운드 사운드</string>
|
||||
<string name="title_frame_pacing">비디오 프레임 처리방식</string>
|
||||
<string name="pacing_smoothness">가장 부드러운 비디오 선호(대기 시간이 크게 증가할 수 있음)</string>
|
||||
<string name="category_help">도움말</string>
|
||||
</resources>
|
||||
@@ -239,4 +239,10 @@
|
||||
<string name="pacing_balanced_alt">Equilibrado com limite de FPS</string>
|
||||
<string name="summary_seekbar_deadzone">Nota: Alguns jogos podem impor uma zona morta maior do que o Moonlight está configurado para usar.</string>
|
||||
<string name="title_checkbox_absolute_mouse_mode">Modo de mouse para área de trabalho remota</string>
|
||||
<string name="resolution_prefix_native_landscape">(Paisagem)</string>
|
||||
<string name="title_checkbox_reduce_refresh_rate">Permitir redução de taxa de atualização</string>
|
||||
<string name="summary_checkbox_reduce_refresh_rate">Taxas menores de atualização do display podem salvar energia ao custo de latência de video adicional</string>
|
||||
<string name="resolution_prefix_native_portrait">(Retrato)</string>
|
||||
<string name="title_checkbox_enable_audiofx">Habilitar sistema de suporte à equalização</string>
|
||||
<string name="summary_checkbox_enable_audiofx">Permitir que efeitos de audio funcionem durante o streaming, pode acrescentar latência de audio</string>
|
||||
</resources>
|
||||
@@ -12,9 +12,15 @@
|
||||
<string name="pcview_menu_header_online">Online</string>
|
||||
<string name="pcview_menu_header_offline">Offline</string>
|
||||
<string name="pcview_menu_app_list">Veja todos aplicativos</string>
|
||||
<string name="pcview_menu_pair_pc">Sincronize com o PC</string>
|
||||
<string name="pcview_menu_pair_pc">Parear com o PC</string>
|
||||
<string name="pcview_menu_details">Ver Detalhes</string>
|
||||
<string name="pair_pairing_title">Pareando</string>
|
||||
<string name="pair_fail">Falha no pareamento</string>
|
||||
<string name="nettest_title_waiting">Testar Conexão com a Internet</string>
|
||||
<string name="pcview_menu_send_wol">Enviar requisição Wake-On-LAN</string>
|
||||
<string name="pcview_menu_delete_pc">Deletar PC</string>
|
||||
<string name="pcview_menu_test_network">Testar Conexão com a Internet</string>
|
||||
<string name="nettest_text_waiting">Moonlight está testando sua conexão com a rede para determinar se o NVIDIA GameStream está bloqueado.
|
||||
\n
|
||||
\nEste processo pode demorar alguns segundos…</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -80,7 +80,7 @@
|
||||
<string name="perf_overlay_dectime">平均解碼時間:%1$.2f ms</string>
|
||||
<!-- AppList activity -->
|
||||
<string name="applist_connect_msg">正在連線電腦…</string>
|
||||
<string name="applist_menu_resume">恢復工作階段</string>
|
||||
<string name="applist_menu_resume">繼續工作階段</string>
|
||||
<string name="applist_menu_quit">結束工作階段</string>
|
||||
<string name="applist_menu_quit_and_start">結束目前遊戲並開始這個遊戲</string>
|
||||
<string name="applist_menu_cancel"> 取消 </string>
|
||||
@@ -146,7 +146,7 @@
|
||||
<string name="summary_checkbox_show_onscreen_controls">在觸控式螢幕上顯示一層虛擬控制器</string>
|
||||
<string name="title_checkbox_vibrate_osc"> 啟用震動 </string>
|
||||
<string name="summary_checkbox_vibrate_osc">使用螢幕控制按鈕時震動裝置以仿真遊戲低頻音</string>
|
||||
<string name="title_only_l3r3">只顯示 L3 和 R3</string>
|
||||
<string name="title_only_l3r3">僅顯示 L3 和 R3</string>
|
||||
<string name="summary_only_l3r3">隱藏除 L3 和 R3 外的所有虛擬按鈕</string>
|
||||
<string name="title_reset_osc">重設已儲存的螢幕控制按鈕版面配置</string>
|
||||
<string name="summary_reset_osc">重設所有螢幕控制按鈕為預設大小和位置</string>
|
||||
@@ -170,9 +170,9 @@
|
||||
<string name="summary_video_format">HEVC 能降低視訊頻寬需求,但需要較新的裝置才能支援</string>
|
||||
<string name="title_enable_hdr">啟用 HDR (實驗性)</string>
|
||||
<string name="summary_enable_hdr">當遊戲和顯示卡支援時以 HDR 模式串流。 HDR 需要 GTX 1000 系列或更高規格顯示卡。</string>
|
||||
<string name="title_enable_perf_overlay">顯示效能資訊</string>
|
||||
<string name="title_enable_perf_overlay">串流時顯示效能資訊</string>
|
||||
<string name="summary_enable_perf_overlay">在串流中顯示即時效能資訊</string>
|
||||
<string name="title_enable_post_stream_toast">串流完畢顯示延時資訊</string>
|
||||
<string name="title_enable_post_stream_toast">串流後顯示延時資訊</string>
|
||||
<string name="summary_enable_post_stream_toast">串流結束後顯示延時資訊</string>
|
||||
<string name="title_osc_opacity">變更螢幕按鈕透明度</string>
|
||||
<string name="dialog_title_osc_opacity">變更不透明度</string>
|
||||
@@ -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>
|
||||
@@ -152,6 +152,8 @@
|
||||
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
|
||||
<string name="resolution_prefix_native">Native</string>
|
||||
<string name="resolution_prefix_native_fullscreen">Native Full-Screen</string>
|
||||
<string name="resolution_prefix_native_landscape">(Landscape)</string>
|
||||
<string name="resolution_prefix_native_portrait">(Portrait)</string>
|
||||
|
||||
<string name="category_audio_settings">Audio Settings</string>
|
||||
<string name="title_audio_config_list">Surround sound configuration</string>
|
||||
@@ -216,6 +218,8 @@
|
||||
<string name="category_advanced_settings">Advanced Settings</string>
|
||||
<string name="title_unlock_fps">Unlock all possible frame rates</string>
|
||||
<string name="summary_unlock_fps">Streaming at 90 or 120 FPS may reduce latency on high-end devices but can cause lag or instability on devices that can\'t support it</string>
|
||||
<string name="title_checkbox_reduce_refresh_rate">Allow refresh rate reduction</string>
|
||||
<string name="summary_checkbox_reduce_refresh_rate">Lower display refresh rates can save power at the expense of some additional video latency</string>
|
||||
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
|
||||
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
|
||||
<string name="title_disable_frame_drop">Never drop frames</string>
|
||||
|
||||
@@ -189,6 +189,11 @@
|
||||
android:title="@string/title_unlock_fps"
|
||||
android:summary="@string/summary_unlock_fps"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_reduce_refresh_rate"
|
||||
android:title="@string/title_checkbox_reduce_refresh_rate"
|
||||
android:summary="@string/summary_checkbox_reduce_refresh_rate"
|
||||
android:defaultValue="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_disable_warnings"
|
||||
android:title="@string/title_checkbox_disable_warnings"
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
- Reduced input latency on devices running Android 11 or later
|
||||
- Refresh rate reduction is now off by default to reduce display latency
|
||||
- Adjusted video decoder buffer processing to slightly reduce decoding latency
|
||||
- Added support for portrait and landscape native resolutions on foldables
|
||||
- Streaming is no longer locked to landscape orientation on foldables
|
||||
- Fixed left clicking using the virtual trackpad on Samsung foldables
|
||||
- Updated community contributed translations from Weblate
|
||||
@@ -0,0 +1,2 @@
|
||||
- Fixed streaming from hosts running Sunshine
|
||||
- Updated community contributed translations from Weblate
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user