Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00415aac79 | |||
| cbe602655c | |||
| 236d8b7030 | |||
| 392e3c7fe3 | |||
| 57f55e6856 | |||
| de54b27013 | |||
| fdc39f0041 | |||
| 7f3b0b03a6 | |||
| 4a6a39dd4c | |||
| 6a8486a076 | |||
| 08a8a3043f | |||
| 7af290b6e1 | |||
| a896f9a28f | |||
| ea003483c4 | |||
| 5b73317e30 | |||
| 1af64b9985 | |||
| af784cf79b | |||
| a2b2131beb | |||
| 2433ce8d24 | |||
| 8b861750e5 | |||
| 99fcd3c669 | |||
| 0ddd8df272 | |||
| a96e508ffb | |||
| 1f21d12d2b | |||
| dd782ac4b2 | |||
| 51594e00b8 | |||
| 6c85f5f8c3 | |||
| d0432de981 | |||
| 2cbc94e51d | |||
| 3ea2aa1f74 | |||
| 1076b516d6 | |||
| 4e87d25851 | |||
| dadd3c7292 | |||
| 9f8abe35f9 | |||
| 0f869a7414 | |||
| aede16c85c | |||
| 61a82e6394 | |||
| 5a92925d6a | |||
| fe697c918f | |||
| bc57a285ce | |||
| 85d8943b64 | |||
| aa6c32968b | |||
| ad1808fb4e | |||
| 576610e4c3 | |||
| ace2266f14 | |||
| 41cedfa6ec | |||
| d46fab33b3 | |||
| 585dc45595 | |||
| c3c9354a00 | |||
| bdc8d08e65 | |||
| 9c792d3272 | |||
| 23bc4daf9f | |||
| fd85ca2004 | |||
| aadf88add1 | |||
| f14ce61ee3 | |||
| 539daf5789 | |||
| 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 |
+9
-2
@@ -9,8 +9,8 @@ android {
|
||||
minSdk 16
|
||||
targetSdk 33
|
||||
|
||||
versionName "10.8.1"
|
||||
versionCode = 288
|
||||
versionName "10.10"
|
||||
versionCode = 298
|
||||
|
||||
// Generate native debug symbols to allow Google Play to symbolicate our native crashes
|
||||
ndk.debugSymbolLevel = 'FULL'
|
||||
@@ -48,6 +48,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
encoding "UTF-8"
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
lint {
|
||||
disable 'MissingTranslation'
|
||||
lintConfig file('lint.xml')
|
||||
@@ -129,6 +135,7 @@ dependencies {
|
||||
implementation 'org.jcodec:jcodec:0.2.3'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.13'
|
||||
implementation 'com.squareup.okio:okio:1.17.5'
|
||||
// 3.5.8 requires minSdk 19, uses StandardCharsets.UTF_8 internally
|
||||
implementation 'org.jmdns:jmdns:3.5.7'
|
||||
implementation 'com.github.cgutman:ShieldControllerExtensions:1.0'
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.limelight.nvstream.NvConnectionListener;
|
||||
import com.limelight.nvstream.StreamConfiguration;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.input.KeyboardPacket;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
@@ -30,7 +31,6 @@ import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.ui.GameGestures;
|
||||
import com.limelight.ui.StreamView;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.NetHelper;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
import com.limelight.utils.ShortcutHelper;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
@@ -87,10 +87,9 @@ import java.util.Locale;
|
||||
|
||||
|
||||
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
|
||||
PerfOverlayListener, UsbDriverService.UsbDriverStateListener
|
||||
{
|
||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
|
||||
PerfOverlayListener, UsbDriverService.UsbDriverStateListener, View.OnKeyListener {
|
||||
private int lastButtonState = 0;
|
||||
|
||||
// Only 2 touches are supported
|
||||
@@ -168,6 +167,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
};
|
||||
|
||||
public static final String EXTRA_HOST = "Host";
|
||||
public static final String EXTRA_PORT = "Port";
|
||||
public static final String EXTRA_HTTPS_PORT = "HttpsPort";
|
||||
public static final String EXTRA_APP_NAME = "AppName";
|
||||
public static final String EXTRA_APP_ID = "AppId";
|
||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||
@@ -232,12 +233,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for events on the game surface
|
||||
// Listen for non-touch events on the game surface
|
||||
streamView = findViewById(R.id.surfaceView);
|
||||
streamView.setOnGenericMotionListener(this);
|
||||
streamView.setOnTouchListener(this);
|
||||
streamView.setOnKeyListener(this);
|
||||
streamView.setInputCallbacks(this);
|
||||
|
||||
// Listen for touch events on the background touch view to enable trackpad mode
|
||||
// to work on areas outside of the StreamView itself. We use a separate View
|
||||
// for this rather than just handling it at the Activity level, because that
|
||||
// allows proper touch splitting, which the OSC relies upon.
|
||||
View backgroundTouchView = findViewById(R.id.backgroundTouchView);
|
||||
backgroundTouchView.setOnTouchListener(this);
|
||||
|
||||
boolean needsInputBatching = false;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Request unbuffered input event dispatching for all input classes we handle here.
|
||||
@@ -250,6 +258,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
InputDevice.SOURCE_CLASS_POSITION | // Touchpads
|
||||
InputDevice.SOURCE_CLASS_TRACKBALL // Mice (pointer capture)
|
||||
);
|
||||
backgroundTouchView.requestUnbufferedDispatch(
|
||||
InputDevice.SOURCE_CLASS_BUTTON | // Keyboards
|
||||
InputDevice.SOURCE_CLASS_JOYSTICK | // Gamepads
|
||||
InputDevice.SOURCE_CLASS_POINTER | // Touchscreens and mice (w/o pointer capture)
|
||||
InputDevice.SOURCE_CLASS_POSITION | // Touchpads
|
||||
InputDevice.SOURCE_CLASS_TRACKBALL // Mice (pointer capture)
|
||||
);
|
||||
|
||||
// Since the OS isn't going to batch for us, we have to batch mouse events to
|
||||
// avoid triggering a bug in GeForce Experience that can lead to massive latency.
|
||||
@@ -263,9 +278,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// The view must be focusable for pointer capture to work.
|
||||
streamView.setFocusable(true);
|
||||
streamView.setDefaultFocusHighlightEnabled(false);
|
||||
streamView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() {
|
||||
@Override
|
||||
public boolean onCapturedPointer(View view, MotionEvent motionEvent) {
|
||||
@@ -302,6 +314,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||
|
||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||
int port = Game.this.getIntent().getIntExtra(EXTRA_PORT, NvHTTP.DEFAULT_HTTP_PORT);
|
||||
int httpsPort = Game.this.getIntent().getIntExtra(EXTRA_HTTPS_PORT, 0); // 0 is treated as unknown
|
||||
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||
@@ -441,11 +455,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
boolean vpnActive = NetHelper.isActiveNetworkVpn(this);
|
||||
if (vpnActive) {
|
||||
LimeLog.info("Detected active network is a VPN");
|
||||
}
|
||||
|
||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||
.setResolution(prefConfig.width, prefConfig.height)
|
||||
.setLaunchRefreshRate(prefConfig.fps)
|
||||
@@ -454,10 +463,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
.setBitrate(prefConfig.bitrate)
|
||||
.setEnableSops(prefConfig.enableSops)
|
||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||
.setMaxPacketSize(vpnActive ? 1024 : 1392) // Lower MTU on VPN
|
||||
.setRemoteConfiguration(vpnActive ? // Use remote optimizations on VPN
|
||||
StreamConfiguration.STREAM_CFG_REMOTE :
|
||||
StreamConfiguration.STREAM_CFG_AUTO)
|
||||
.setMaxPacketSize(1392)
|
||||
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO) // NvConnection will perform LAN and VPN detection
|
||||
.setHevcBitratePercentageMultiplier(75)
|
||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||
.setEnableHdr(willStreamHdr)
|
||||
@@ -465,10 +472,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
|
||||
.setAudioConfiguration(prefConfig.audioConfiguration)
|
||||
.setAudioEncryption(true)
|
||||
.setColorSpace(decoderRenderer.getPreferredColorSpace())
|
||||
.setColorRange(decoderRenderer.getPreferredColorRange())
|
||||
.build();
|
||||
|
||||
// Initialize the connection
|
||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert, needsInputBatching);
|
||||
conn = new NvConnection(getApplicationContext(),
|
||||
new ComputerDetails.AddressTuple(host, port),
|
||||
httpsPort, uniqueId, config,
|
||||
PlatformBinding.getCryptoProvider(this), serverCert,
|
||||
needsInputBatching);
|
||||
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
||||
keyboardTranslator = new KeyboardTranslator();
|
||||
|
||||
@@ -686,7 +699,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
if (manager != null) {
|
||||
Class<?>[] parameterTypes = new Class<?>[2];
|
||||
parameterTypes[0] = String.class;
|
||||
parameterTypes[0] = ComponentName.class;
|
||||
parameterTypes[1] = boolean.class;
|
||||
Method requestMetaKeyEventMethod = semWindowManager.getDeclaredMethod("requestMetaKeyEvent", parameterTypes);
|
||||
requestMetaKeyEventMethod.invoke(manager, this.getComponentName(), enabled);
|
||||
@@ -1574,15 +1587,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return true;
|
||||
}
|
||||
|
||||
if (view == null && !prefConfig.touchscreenTrackpad) {
|
||||
// Absolute touch events should be dropped outside our view.
|
||||
return true;
|
||||
// If this is the parent view, we'll offset our coordinates to appear as if they
|
||||
// are relative to the StreamView like our StreamView touch events are.
|
||||
float xOffset, yOffset;
|
||||
if (view != streamView && !prefConfig.touchscreenTrackpad) {
|
||||
xOffset = -streamView.getX();
|
||||
yOffset = -streamView.getY();
|
||||
}
|
||||
else {
|
||||
xOffset = 0.f;
|
||||
yOffset = 0.f;
|
||||
}
|
||||
|
||||
int actionIndex = event.getActionIndex();
|
||||
|
||||
int eventX = (int)event.getX(actionIndex);
|
||||
int eventY = (int)event.getY(actionIndex);
|
||||
int eventX = (int)(event.getX(actionIndex) + xOffset);
|
||||
int eventY = (int)(event.getY(actionIndex) + yOffset);
|
||||
|
||||
// Special handling for 3 finger gesture
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN &&
|
||||
@@ -1637,7 +1657,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
|
||||
// The original secondary touch now becomes primary
|
||||
context.touchDownEvent((int)event.getX(1), (int)event.getY(1), event.getEventTime(), false);
|
||||
context.touchDownEvent(
|
||||
(int)(event.getX(1) + xOffset),
|
||||
(int)(event.getY(1) + yOffset),
|
||||
event.getEventTime(), false);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
@@ -1650,8 +1673,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
|
||||
{
|
||||
aTouchContextMap.touchMoveEvent(
|
||||
(int)event.getHistoricalX(aTouchContextMap.getActionIndex(), i),
|
||||
(int)event.getHistoricalY(aTouchContextMap.getActionIndex(), i),
|
||||
(int)(event.getHistoricalX(aTouchContextMap.getActionIndex(), i) + xOffset),
|
||||
(int)(event.getHistoricalY(aTouchContextMap.getActionIndex(), i) + yOffset),
|
||||
event.getHistoricalEventTime(i));
|
||||
}
|
||||
}
|
||||
@@ -1662,8 +1685,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
|
||||
{
|
||||
aTouchContextMap.touchMoveEvent(
|
||||
(int)event.getX(aTouchContextMap.getActionIndex()),
|
||||
(int)event.getY(aTouchContextMap.getActionIndex()),
|
||||
(int)(event.getX(aTouchContextMap.getActionIndex()) + xOffset),
|
||||
(int)(event.getY(aTouchContextMap.getActionIndex()) + yOffset),
|
||||
event.getEventTime());
|
||||
}
|
||||
}
|
||||
@@ -1687,22 +1710,27 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return handleMotionEvent(null, event) || super.onTouchEvent(event);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
return handleMotionEvent(null, event) || super.onGenericMotionEvent(event);
|
||||
|
||||
}
|
||||
|
||||
private void updateMousePosition(View view, MotionEvent event) {
|
||||
private void updateMousePosition(View touchedView, MotionEvent event) {
|
||||
// X and Y are already relative to the provided view object
|
||||
float eventX = event.getX(0);
|
||||
float eventY = event.getY(0);
|
||||
float eventX, eventY;
|
||||
|
||||
// For our StreamView itself, we can use the coordinates unmodified.
|
||||
if (touchedView == streamView) {
|
||||
eventX = event.getX(0);
|
||||
eventY = event.getY(0);
|
||||
}
|
||||
else {
|
||||
// For the containing background view, we must subtract the origin
|
||||
// of the StreamView to get video-relative coordinates.
|
||||
eventX = event.getX(0) - streamView.getX();
|
||||
eventY = event.getY(0) - streamView.getY();
|
||||
}
|
||||
|
||||
if (event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
|
||||
(event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER ||
|
||||
@@ -1735,10 +1763,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Normalize these to the view size. We can't just drop them because we won't always get an event
|
||||
// right at the boundary of the view, so dropping them would result in our cursor never really
|
||||
// reaching the sides of the screen.
|
||||
eventX = Math.min(Math.max(eventX, 0), view.getWidth());
|
||||
eventY = Math.min(Math.max(eventY, 0), view.getHeight());
|
||||
eventX = Math.min(Math.max(eventX, 0), streamView.getWidth());
|
||||
eventY = Math.min(Math.max(eventY, 0), streamView.getHeight());
|
||||
|
||||
conn.sendMousePosition((short)eventX, (short)eventY, (short)view.getWidth(), (short)view.getHeight());
|
||||
conn.sendMousePosition((short)eventX, (short)eventY, (short)streamView.getWidth(), (short)streamView.getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1757,6 +1785,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
view.requestUnbufferedDispatch(event);
|
||||
}
|
||||
}
|
||||
|
||||
return handleMotionEvent(view, event);
|
||||
}
|
||||
|
||||
@@ -1887,6 +1916,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
message = getResources().getString(R.string.early_termination_error);
|
||||
break;
|
||||
|
||||
case MoonBridge.ML_ERROR_FRAME_CONVERSION:
|
||||
message = getResources().getString(R.string.frame_conversion_error);
|
||||
break;
|
||||
|
||||
default:
|
||||
message = getResources().getString(R.string.conn_terminated_msg);
|
||||
break;
|
||||
@@ -2167,4 +2200,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
suppressPipRefCount--;
|
||||
updatePipAutoEnter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
|
||||
switch (keyEvent.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
return handleKeyDown(keyEvent);
|
||||
case KeyEvent.ACTION_UP:
|
||||
return handleKeyUp(keyEvent);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,10 +383,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (computer.runningGameId != 0) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
if (managerBinder == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
@@ -404,8 +400,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
stopComputerUpdates(true);
|
||||
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
computer.serverCert,
|
||||
computer.httpsPort, managerBinder.getUniqueId(), computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairState.PAIRED) {
|
||||
// Don't display any toast, but open the app list
|
||||
@@ -421,12 +416,17 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
PairingManager pm = httpConn.getPairingManager();
|
||||
|
||||
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
|
||||
PairState pairState = pm.pair(httpConn.getServerInfo(true), pinStr);
|
||||
if (pairState == PairState.PIN_WRONG) {
|
||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||
}
|
||||
else if (pairState == PairState.FAILED) {
|
||||
message = getResources().getString(R.string.pair_fail);
|
||||
if (computer.runningGameId != 0) {
|
||||
message = getResources().getString(R.string.pair_pc_ingame);
|
||||
}
|
||||
else {
|
||||
message = getResources().getString(R.string.pair_fail);
|
||||
}
|
||||
}
|
||||
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
|
||||
message = getResources().getString(R.string.pair_already_in_progress);
|
||||
@@ -533,8 +533,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
computer.serverCert,
|
||||
computer.httpsPort, managerBinder.getUniqueId(), computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
httpConn.unpair();
|
||||
|
||||
@@ -67,14 +67,12 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FileInputStream fin = new FileInputStream(f);
|
||||
try (final FileInputStream fin = new FileInputStream(f)) {
|
||||
byte[] fileData = new byte[(int) f.length()];
|
||||
if (fin.read(fileData) != f.length()) {
|
||||
// Failed to read
|
||||
fileData = null;
|
||||
}
|
||||
fin.close();
|
||||
return fileData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
@@ -160,32 +158,28 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
}
|
||||
|
||||
private void saveCertKeyPair() {
|
||||
try {
|
||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||
|
||||
try (final FileOutputStream certOut = new FileOutputStream(certFile);
|
||||
final FileOutputStream keyOut = new FileOutputStream(keyFile)
|
||||
) {
|
||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||
StringWriter strWriter = new StringWriter();
|
||||
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||
pemWriter.writeObject(cert);
|
||||
pemWriter.close();
|
||||
try (final JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter)) {
|
||||
pemWriter.writeObject(cert);
|
||||
}
|
||||
|
||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||
String pemStr = strWriter.getBuffer().toString();
|
||||
for (int i = 0; i < pemStr.length(); i++) {
|
||||
char c = pemStr.charAt(i);
|
||||
if (c != '\r')
|
||||
certWriter.append(c);
|
||||
try (final OutputStreamWriter certWriter = new OutputStreamWriter(certOut)) {
|
||||
String pemStr = strWriter.getBuffer().toString();
|
||||
for (int i = 0; i < pemStr.length(); i++) {
|
||||
char c = pemStr.charAt(i);
|
||||
if (c != '\r')
|
||||
certWriter.append(c);
|
||||
}
|
||||
}
|
||||
certWriter.close();
|
||||
|
||||
// Write the private out in PKCS8 format
|
||||
keyOut.write(key.getEncoded());
|
||||
|
||||
certOut.close();
|
||||
keyOut.close();
|
||||
|
||||
LimeLog.info("Saved generated key pair to disk");
|
||||
} catch (IOException e) {
|
||||
// This isn't good because it means we'll have
|
||||
|
||||
@@ -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,14 +1,16 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.jcodec.codecs.h264.H264Utils;
|
||||
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
||||
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
||||
|
||||
import com.limelight.BuildConfig;
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
@@ -71,6 +73,24 @@ 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_FLUSH = 1;
|
||||
private static final int CR_RECOVERY_TYPE_RESTART = 2;
|
||||
private static final int CR_RECOVERY_TYPE_RESET = 3;
|
||||
private AtomicInteger codecRecoveryType = new AtomicInteger(CR_RECOVERY_TYPE_NONE);
|
||||
private final Object codecRecoveryMonitor = new Object();
|
||||
|
||||
// Each thread that touches the MediaCodec object or any associated buffers must have a flag
|
||||
// here and must call doCodecRecoveryIfRequired() on a regular basis.
|
||||
private static final int CR_FLAG_INPUT_THREAD = 0x1;
|
||||
private static final int CR_FLAG_RENDER_THREAD = 0x2;
|
||||
private static final int CR_FLAG_CHOREOGRAPHER = 0x4;
|
||||
private static final int CR_FLAG_ALL = CR_FLAG_INPUT_THREAD | CR_FLAG_RENDER_THREAD | CR_FLAG_CHOREOGRAPHER;
|
||||
private int codecRecoveryThreadQuiescedFlags = 0;
|
||||
private int codecRecoveryAttempts = 0;
|
||||
|
||||
private MediaFormat inputFormat;
|
||||
private MediaFormat outputFormat;
|
||||
private MediaFormat configuredFormat;
|
||||
@@ -179,7 +199,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
// for even required levels of HEVC.
|
||||
MediaCodecInfo hevcDecoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
||||
if (hevcDecoderInfo != null) {
|
||||
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(hevcDecoderInfo.getName(), meteredNetwork, prefs)) {
|
||||
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(hevcDecoderInfo)) {
|
||||
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+hevcDecoderInfo.getName());
|
||||
|
||||
// Force HEVC enabled if the user asked for it
|
||||
@@ -264,7 +284,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
}
|
||||
|
||||
if (hevcDecoder != null) {
|
||||
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(hevcDecoder.getName());
|
||||
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(hevcDecoder);
|
||||
hevcOptimalSlicesPerFrame = MediaCodecHelper.getDecoderOptimalSlicesPerFrame(hevcDecoder.getName());
|
||||
|
||||
if (refFrameInvalidationHevc) {
|
||||
@@ -307,6 +327,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getPreferredColorSpace() {
|
||||
return MoonBridge.COLORSPACE_REC_601;
|
||||
}
|
||||
|
||||
public int getPreferredColorRange() {
|
||||
return MoonBridge.COLOR_RANGE_LIMITED;
|
||||
}
|
||||
|
||||
public void notifyVideoForeground() {
|
||||
foreground = true;
|
||||
}
|
||||
@@ -337,68 +365,68 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
return videoFormat;
|
||||
}
|
||||
|
||||
private boolean tryConfigureDecoder(MediaCodecInfo selectedDecoderInfo, MediaFormat format) {
|
||||
try {
|
||||
videoDecoder = MediaCodec.createByCodecName(selectedDecoderInfo.getName());
|
||||
LimeLog.info("Configuring with format: "+format);
|
||||
private void configureAndStartDecoder(MediaFormat format) {
|
||||
LimeLog.info("Configuring with format: "+format);
|
||||
|
||||
videoDecoder.configure(format, renderTarget.getSurface(), null, 0);
|
||||
videoDecoder.configure(format, renderTarget.getSurface(), null, 0);
|
||||
|
||||
configuredFormat = format;
|
||||
configuredFormat = format;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// This will contain the actual accepted input format attributes
|
||||
inputFormat = videoDecoder.getInputFormat();
|
||||
LimeLog.info("Input format: "+inputFormat);
|
||||
}
|
||||
// After reconfiguration, we must resubmit CSD buffers
|
||||
submittedCsd = false;
|
||||
submitCsdNextCall = false;
|
||||
vpsBuffer = null;
|
||||
spsBuffer = null;
|
||||
ppsBuffer = null;
|
||||
|
||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// This will contain the actual accepted input format attributes
|
||||
inputFormat = videoDecoder.getInputFormat();
|
||||
LimeLog.info("Input format: "+inputFormat);
|
||||
}
|
||||
|
||||
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
videoDecoder.setOnFrameRenderedListener(new MediaCodec.OnFrameRenderedListener() {
|
||||
@Override
|
||||
public void onFrameRendered(MediaCodec mediaCodec, long presentationTimeUs, long renderTimeNanos) {
|
||||
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
if (USE_FRAME_RENDER_TIME) {
|
||||
activeWindowVideoStats.totalTimeMs += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
|
||||
LimeLog.info("Using codec "+selectedDecoderInfo.getName()+" for hardware decoding "+format.getString(MediaFormat.KEY_MIME));
|
||||
// Start the decoder
|
||||
videoDecoder.start();
|
||||
|
||||
// Start the decoder
|
||||
videoDecoder.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
legacyInputBuffers = videoDecoder.getInputBuffers();
|
||||
}
|
||||
|
||||
fetchNextInputBuffer();
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
||||
if (videoDecoder != null) {
|
||||
videoDecoder.release();
|
||||
videoDecoder = null;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
legacyInputBuffers = videoDecoder.getInputBuffers();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int setup(int format, int width, int height, int redrawRate) {
|
||||
this.initialWidth = width;
|
||||
this.initialHeight = height;
|
||||
this.videoFormat = format;
|
||||
this.refreshRate = redrawRate;
|
||||
private boolean tryConfigureDecoder(MediaCodecInfo selectedDecoderInfo, MediaFormat format, boolean throwOnCodecError) {
|
||||
boolean configured = false;
|
||||
try {
|
||||
videoDecoder = MediaCodec.createByCodecName(selectedDecoderInfo.getName());
|
||||
configureAndStartDecoder(format);
|
||||
LimeLog.info("Using codec " + selectedDecoderInfo.getName() + " for hardware decoding " + format.getString(MediaFormat.KEY_MIME));
|
||||
configured = true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
if (throwOnCodecError) {
|
||||
throw e;
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
if (throwOnCodecError) {
|
||||
throw e;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
if (throwOnCodecError) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} finally {
|
||||
if (!configured && videoDecoder != null) {
|
||||
videoDecoder.release();
|
||||
videoDecoder = null;
|
||||
}
|
||||
}
|
||||
return configured;
|
||||
}
|
||||
|
||||
public int initializeDecoder(boolean throwOnCodecError) {
|
||||
String mimeType;
|
||||
MediaCodecInfo selectedDecoderInfo;
|
||||
|
||||
@@ -411,7 +439,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (width > 4096 || height > 4096) {
|
||||
if (initialWidth > 4096 || initialHeight > 4096) {
|
||||
LimeLog.severe("> 4K streaming only supported on HEVC");
|
||||
return -1;
|
||||
}
|
||||
@@ -464,7 +492,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
// This will try low latency options until we find one that works (or we give up).
|
||||
boolean newFormat = MediaCodecHelper.setDecoderLowLatencyOptions(mediaFormat, selectedDecoderInfo, tryNumber);
|
||||
|
||||
if (tryConfigureDecoder(selectedDecoderInfo, mediaFormat)) {
|
||||
// Throw the underlying codec exception on the last attempt if the caller requested it
|
||||
if (tryConfigureDecoder(selectedDecoderInfo, mediaFormat, !newFormat && throwOnCodecError)) {
|
||||
// Success!
|
||||
break;
|
||||
}
|
||||
@@ -475,26 +504,268 @@ 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;
|
||||
|
||||
// This is the final thread to quiesce, so let's perform the codec recovery now.
|
||||
if (codecRecoveryThreadQuiescedFlags == CR_FLAG_ALL) {
|
||||
// Input and output buffers are invalidated by stop() and reset().
|
||||
nextInputBuffer = null;
|
||||
nextInputBufferIndex = -1;
|
||||
outputBufferQueue.clear();
|
||||
|
||||
// If we just need a flush, do so now with all threads quiesced.
|
||||
if (codecRecoveryType.get() == CR_RECOVERY_TYPE_FLUSH) {
|
||||
LimeLog.warning("Flushing decoder");
|
||||
try {
|
||||
videoDecoder.flush();
|
||||
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_RESTART);
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.severe(codecExc.getDiagnosticInfo());
|
||||
// We don't count flushes as codec recovery attempts
|
||||
if (codecRecoveryType.get() != CR_RECOVERY_TYPE_NONE) {
|
||||
codecRecoveryAttempts++;
|
||||
LimeLog.info("Codec recovery attempt: "+codecRecoveryAttempts);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
if (codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_NONE, CR_RECOVERY_TYPE_RESTART)) {
|
||||
LimeLog.info("Decoder requires restart for recoverable CodecException");
|
||||
e.printStackTrace();
|
||||
}
|
||||
else if (codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_FLUSH, CR_RECOVERY_TYPE_RESTART)) {
|
||||
LimeLog.info("Decoder flush promoted to restart for recoverable CodecException");
|
||||
e.printStackTrace();
|
||||
}
|
||||
else if (codecRecoveryType.get() != CR_RECOVERY_TYPE_RESET && codecRecoveryType.get() != CR_RECOVERY_TYPE_RESTART) {
|
||||
throw new IllegalStateException("Unexpected codec recovery type: " + codecRecoveryType.get());
|
||||
}
|
||||
}
|
||||
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_FLUSH, CR_RECOVERY_TYPE_RESET)) {
|
||||
LimeLog.info("Decoder flush promoted to 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_FLUSH, CR_RECOVERY_TYPE_RESET)) {
|
||||
LimeLog.info("Decoder flush promoted to reset for IllegalStateException");
|
||||
e.printStackTrace();
|
||||
}
|
||||
else if (codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_RESTART, CR_RECOVERY_TYPE_RESET)) {
|
||||
LimeLog.info("Decoder restart promoted to reset for IllegalStateException");
|
||||
e.printStackTrace();
|
||||
}
|
||||
else if (codecRecoveryType.get() != CR_RECOVERY_TYPE_RESET) {
|
||||
throw new IllegalStateException("Unexpected codec recovery type: " + codecRecoveryType.get());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only throw if we're not in the middle of codec recovery
|
||||
if (codecRecoveryType.get() == CR_RECOVERY_TYPE_NONE) {
|
||||
//
|
||||
// There seems to be a race condition with decoder/surface teardown causing some
|
||||
// decoders to to throw IllegalStateExceptions even before 'stopping' is set.
|
||||
@@ -515,15 +786,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
}
|
||||
else {
|
||||
// This is the first exception we've hit
|
||||
if (buf != null || codecFlags != 0) {
|
||||
initialException = new RendererException(this, e, buf, codecFlags);
|
||||
}
|
||||
else {
|
||||
initialException = new RendererException(this, e);
|
||||
}
|
||||
initialException = new RendererException(this, e);
|
||||
initialExceptionTimestamp = SystemClock.uptimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
// Not transient
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -555,13 +824,23 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
|
||||
lastRenderedFrameTimeNanos = frameTimeNanos;
|
||||
activeWindowVideoStats.totalFramesRendered++;
|
||||
} catch (Exception e) {
|
||||
// This will leak nextOutputBuffer, but there's really nothing else we can do
|
||||
handleDecoderException(e, null, 0, false);
|
||||
} catch (IllegalStateException ignored) {
|
||||
try {
|
||||
// Try to avoid leaking the output buffer by releasing it without rendering
|
||||
videoDecoder.releaseOutputBuffer(nextOutputBuffer, false);
|
||||
} catch (IllegalStateException e) {
|
||||
// This will leak nextOutputBuffer, but there's really nothing else we can do
|
||||
e.printStackTrace();
|
||||
handleDecoderException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt codec recovery even if we have nothing to render right now. Recovery can still
|
||||
// be required even if the codec died before giving any output.
|
||||
doCodecRecoveryIfRequired(CR_FLAG_CHOREOGRAPHER);
|
||||
|
||||
// Request another callback for next frame
|
||||
Choreographer.getInstance().postFrameCallback(this);
|
||||
}
|
||||
@@ -648,7 +927,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
// run for a while (if there is a huge mismatch between stream FPS and display
|
||||
// refresh rate).
|
||||
if (outputBufferQueue.size() == OUTPUT_BUFFER_QUEUE_LIMIT) {
|
||||
videoDecoder.releaseOutputBuffer(outputBufferQueue.take(), false);
|
||||
try {
|
||||
videoDecoder.releaseOutputBuffer(outputBufferQueue.take(), false);
|
||||
} catch (InterruptedException e) {
|
||||
// We're shutting down, so we can just drop this buffer on the floor
|
||||
// and it will be reclaimed when the codec is released.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add this buffer
|
||||
@@ -676,8 +961,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, 0, false);
|
||||
} catch (IllegalStateException e) {
|
||||
handleDecoderException(e);
|
||||
} finally {
|
||||
doCodecRecoveryIfRequired(CR_FLAG_RENDER_THREAD);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,8 +976,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
|
||||
private boolean fetchNextInputBuffer() {
|
||||
long startTime;
|
||||
boolean codecRecovered;
|
||||
|
||||
if (nextInputBufferIndex >= 0) {
|
||||
if (nextInputBuffer != null) {
|
||||
// We already have an input buffer
|
||||
return true;
|
||||
}
|
||||
@@ -698,15 +986,23 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
startTime = SystemClock.uptimeMillis();
|
||||
|
||||
try {
|
||||
// If we don't have an input buffer index yet, fetch one now
|
||||
while (nextInputBufferIndex < 0 && !stopping) {
|
||||
nextInputBufferIndex = videoDecoder.dequeueInputBuffer(10000);
|
||||
}
|
||||
|
||||
// Get the backing ByteBuffer for the input buffer index
|
||||
if (nextInputBufferIndex >= 0) {
|
||||
// Using the new getInputBuffer() API on Lollipop allows
|
||||
// the framework to do some performance optimizations for us
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
nextInputBuffer = videoDecoder.getInputBuffer(nextInputBufferIndex);
|
||||
if (nextInputBuffer == null) {
|
||||
// According to the Android docs, getInputBuffer() can return null "if the
|
||||
// index is not a dequeued input buffer". I don't think this ever should
|
||||
// happen but if it does, let's try to get a new input buffer next time.
|
||||
nextInputBufferIndex = -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
nextInputBuffer = legacyInputBuffers[nextInputBufferIndex];
|
||||
@@ -715,8 +1011,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
nextInputBuffer.clear();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, 0, true);
|
||||
} catch (IllegalStateException e) {
|
||||
handleDecoderException(e);
|
||||
return false;
|
||||
} finally {
|
||||
codecRecovered = doCodecRecoveryIfRequired(CR_FLAG_INPUT_THREAD);
|
||||
}
|
||||
|
||||
// If codec recovery is required, always return false to ensure the caller will request
|
||||
// an IDR frame to complete the codec recovery.
|
||||
if (codecRecovered) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -726,7 +1030,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
LimeLog.warning("Dequeue input buffer ran long: " + deltaMs + " ms");
|
||||
}
|
||||
|
||||
if (nextInputBufferIndex < 0) {
|
||||
if (nextInputBuffer == null) {
|
||||
// We've been hung for 5 seconds and no other exception was reported,
|
||||
// so generate a decoder hung exception
|
||||
if (deltaMs >= 5000 && initialException == null) {
|
||||
@@ -760,6 +1064,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
rendererThread.interrupt();
|
||||
}
|
||||
|
||||
// Stop any active codec recovery operations
|
||||
synchronized (codecRecoveryMonitor) {
|
||||
codecRecoveryType.set(CR_RECOVERY_TYPE_NONE);
|
||||
codecRecoveryMonitor.notifyAll();
|
||||
}
|
||||
|
||||
// Post a quit message to the Choreographer looper (if we have one)
|
||||
if (choreographerHandler != null) {
|
||||
choreographerHandler.post(new Runnable() {
|
||||
@@ -818,23 +1128,47 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
}
|
||||
|
||||
private boolean queueNextInputBuffer(long timestampUs, int codecFlags) {
|
||||
boolean codecRecovered;
|
||||
|
||||
try {
|
||||
videoDecoder.queueInputBuffer(nextInputBufferIndex,
|
||||
0, nextInputBuffer.position(),
|
||||
timestampUs, codecFlags);
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, codecFlags, true);
|
||||
return false;
|
||||
} finally {
|
||||
|
||||
// We need a new buffer now
|
||||
nextInputBufferIndex = -1;
|
||||
nextInputBuffer = null;
|
||||
} catch (IllegalStateException e) {
|
||||
if (handleDecoderException(e)) {
|
||||
// We encountered a transient error. In this case, just hold onto the buffer
|
||||
// (to avoid leaking it), clear it, and keep it for the next frame. We'll return
|
||||
// false to trigger an IDR frame to recover.
|
||||
nextInputBuffer.clear();
|
||||
}
|
||||
else {
|
||||
// We encountered a non-transient error. In this case, we will simply leak the
|
||||
// buffer because we cannot be sure we will ever succeed in queuing it.
|
||||
nextInputBufferIndex = -1;
|
||||
nextInputBuffer = null;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
codecRecovered = doCodecRecoveryIfRequired(CR_FLAG_INPUT_THREAD);
|
||||
}
|
||||
|
||||
// If codec recovery is required, always return false to ensure the caller will request
|
||||
// an IDR frame to complete the codec recovery.
|
||||
if (codecRecovered) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch a new input buffer now while we have some time between frames
|
||||
// to have it ready immediately when the next frame arrives.
|
||||
fetchNextInputBuffer();
|
||||
|
||||
return true;
|
||||
//
|
||||
// We must propagate the return value here in order to properly handle
|
||||
// codec recovery happening in fetchNextInputBuffer(). If we don't, we'll
|
||||
// never get an IDR frame to complete the recovery process.
|
||||
return fetchNextInputBuffer();
|
||||
}
|
||||
|
||||
private void doProfileSpecificSpsPatching(SeqParameterSet sps) {
|
||||
@@ -971,7 +1305,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
|
||||
// GFE 2.5.11 changed the SPS to add additional extensions
|
||||
// Some devices don't like these so we remove them here on old devices.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && sps.vuiParams != null) {
|
||||
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||
@@ -983,11 +1317,19 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||
|
||||
// If the encoder didn't include VUI parameters in the SPS, add them now
|
||||
if (sps.vuiParams == null) {
|
||||
LimeLog.info("Adding VUI parameters");
|
||||
sps.vuiParams = new VUIParameters();
|
||||
}
|
||||
|
||||
// GFE 2.5.11 started sending bitstream restrictions
|
||||
if (sps.vuiParams.bitstreamRestriction == null) {
|
||||
LimeLog.info("Adding bitstream restrictions");
|
||||
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
||||
sps.vuiParams.bitstreamRestriction.motionVectorsOverPicBoundariesFlag = true;
|
||||
sps.vuiParams.bitstreamRestriction.maxBytesPerPicDenom = 2;
|
||||
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
|
||||
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthHorizontal = 16;
|
||||
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthVertical = 16;
|
||||
sps.vuiParams.bitstreamRestriction.numReorderFrames = 0;
|
||||
@@ -1001,13 +1343,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
|
||||
// These values are the defaults for the fields, but they are more aggressive
|
||||
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
|
||||
sps.vuiParams.bitstreamRestriction.maxBytesPerPicDenom = 2;
|
||||
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
|
||||
// We'll leave these alone for "modern" devices just in case they care.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
sps.vuiParams.bitstreamRestriction.maxBytesPerPicDenom = 2;
|
||||
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 1;
|
||||
}
|
||||
|
||||
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
|
||||
// conservative values by GFE 2.5.11. We'll let those values stand.
|
||||
}
|
||||
else {
|
||||
else if (sps.vuiParams != null) {
|
||||
// Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11
|
||||
// will continue to not receive them now
|
||||
sps.vuiParams.bitstreamRestriction = null;
|
||||
@@ -1238,7 +1583,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
public String toString() {
|
||||
String str = "";
|
||||
|
||||
str += "Hang time: "+hangTimeMs+" ms\n";
|
||||
str += "Hang time: "+hangTimeMs+" ms"+ RendererException.DELIMITER;
|
||||
str += super.toString();
|
||||
|
||||
return str;
|
||||
@@ -1247,22 +1592,19 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
|
||||
static class RendererException extends RuntimeException {
|
||||
private static final long serialVersionUID = 8985937536997012406L;
|
||||
protected static final String DELIMITER = BuildConfig.DEBUG ? "\n" : " | ";
|
||||
|
||||
private String text;
|
||||
|
||||
RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
|
||||
this.text = generateText(renderer, e, null, 0);
|
||||
}
|
||||
|
||||
RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
|
||||
this.text = generateText(renderer, e, currentBuffer, currentCodecFlags);
|
||||
this.text = generateText(renderer, e);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return text;
|
||||
}
|
||||
|
||||
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) {
|
||||
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException) {
|
||||
String str;
|
||||
|
||||
if (renderer.numVpsIn == 0 && renderer.numSpsIn == 0 && renderer.numPpsIn == 0) {
|
||||
@@ -1287,44 +1629,43 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
str = "ErrorWhileStreaming";
|
||||
}
|
||||
|
||||
str += ": 1\n";
|
||||
str += "Format: "+String.format("%x", renderer.videoFormat)+"\n";
|
||||
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
||||
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
||||
str += "Format: "+String.format("%x", renderer.videoFormat)+DELIMITER;
|
||||
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+DELIMITER;
|
||||
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+DELIMITER;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
|
||||
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
||||
str += "AVC supported width range: "+avcWidthRange+"\n";
|
||||
str += "AVC supported width range: "+avcWidthRange+DELIMITER;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
Range<Double> avcFpsRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
|
||||
str += "AVC achievable FPS range: "+avcFpsRange+"\n";
|
||||
str += "AVC achievable FPS range: "+avcFpsRange+DELIMITER;
|
||||
} catch (IllegalArgumentException e) {
|
||||
str += "AVC achievable FPS range: UNSUPPORTED!\n";
|
||||
str += "AVC achievable FPS range: UNSUPPORTED!"+DELIMITER;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
|
||||
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
||||
str += "HEVC supported width range: "+hevcWidthRange+"\n";
|
||||
str += "HEVC supported width range: "+hevcWidthRange+DELIMITER;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
Range<Double> hevcFpsRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
|
||||
str += "HEVC achievable FPS range: " + hevcFpsRange + "\n";
|
||||
str += "HEVC achievable FPS range: " + hevcFpsRange + DELIMITER;
|
||||
} catch (IllegalArgumentException e) {
|
||||
str += "HEVC achievable FPS range: UNSUPPORTED!\n";
|
||||
str += "HEVC achievable FPS range: UNSUPPORTED!"+DELIMITER;
|
||||
}
|
||||
}
|
||||
}
|
||||
str += "Configured format: "+renderer.configuredFormat+"\n";
|
||||
str += "Input format: "+renderer.inputFormat+"\n";
|
||||
str += "Output format: "+renderer.outputFormat+"\n";
|
||||
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
|
||||
str += "GL Renderer: "+renderer.glRenderer+"\n";
|
||||
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
||||
str += "Configured format: "+renderer.configuredFormat+DELIMITER;
|
||||
str += "Input format: "+renderer.inputFormat+DELIMITER;
|
||||
str += "Output format: "+renderer.outputFormat+DELIMITER;
|
||||
str += "Adaptive playback: "+renderer.adaptivePlayback+DELIMITER;
|
||||
str += "GL Renderer: "+renderer.glRenderer+DELIMITER;
|
||||
//str += "Build fingerprint: "+Build.FINGERPRINT+DELIMITER;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
str += "SOC: "+Build.SOC_MANUFACTURER+" - "+Build.SOC_MODEL+"\n";
|
||||
str += "Performance class: "+Build.VERSION.MEDIA_PERFORMANCE_CLASS+"\n";
|
||||
str += "Vendor params: ";
|
||||
str += "SOC: "+Build.SOC_MANUFACTURER+" - "+Build.SOC_MODEL+DELIMITER;
|
||||
str += "Performance class: "+Build.VERSION.MEDIA_PERFORMANCE_CLASS+DELIMITER;
|
||||
/*str += "Vendor params: ";
|
||||
List<String> params = renderer.videoDecoder.getSupportedVendorParameters();
|
||||
if (params.isEmpty()) {
|
||||
str += "NONE";
|
||||
@@ -1334,65 +1675,38 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C
|
||||
str += param + " ";
|
||||
}
|
||||
}
|
||||
str += "\n";
|
||||
str += DELIMITER;*/
|
||||
}
|
||||
str += "Foreground: "+renderer.foreground+"\n";
|
||||
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
||||
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
|
||||
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n";
|
||||
str += "Fused IDR frames: "+renderer.fusedIdrFrame+"\n";
|
||||
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||
str += "FPS target: "+renderer.refreshRate+"\n";
|
||||
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
||||
str += "CSD stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
||||
str += "Frames in-out: "+renderer.numFramesIn+", "+renderer.numFramesOut+"\n";
|
||||
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
|
||||
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
|
||||
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n";
|
||||
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
|
||||
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
|
||||
str += "Frame pacing mode: "+renderer.prefs.framePacing+"\n";
|
||||
|
||||
if (currentBuffer != null) {
|
||||
str += "Current buffer: ";
|
||||
currentBuffer.flip();
|
||||
while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) {
|
||||
str += String.format((Locale)null, "%02x ", currentBuffer.get());
|
||||
}
|
||||
str += "\n";
|
||||
str += "Buffer codec flags: "+currentCodecFlags+"\n";
|
||||
}
|
||||
|
||||
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
|
||||
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+DELIMITER;
|
||||
str += "RFI active: "+renderer.refFrameInvalidationActive+DELIMITER;
|
||||
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+DELIMITER;
|
||||
str += "Fused IDR frames: "+renderer.fusedIdrFrame+DELIMITER;
|
||||
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+DELIMITER;
|
||||
str += "FPS target: "+renderer.refreshRate+DELIMITER;
|
||||
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps"+DELIMITER;
|
||||
str += "CSD stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+DELIMITER;
|
||||
str += "Frames in-out: "+renderer.numFramesIn+", "+renderer.numFramesOut+DELIMITER;
|
||||
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+DELIMITER;
|
||||
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+DELIMITER;
|
||||
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events"+DELIMITER;
|
||||
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms"+DELIMITER;
|
||||
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms"+DELIMITER;
|
||||
str += "Frame pacing mode: "+renderer.prefs.framePacing+DELIMITER;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (originalException instanceof CodecException) {
|
||||
CodecException ce = (CodecException) originalException;
|
||||
|
||||
str += "Diagnostic Info: "+ce.getDiagnosticInfo()+"\n";
|
||||
str += "Recoverable: "+ce.isRecoverable()+"\n";
|
||||
str += "Transient: "+ce.isTransient()+"\n";
|
||||
str += "Diagnostic Info: "+ce.getDiagnosticInfo()+DELIMITER;
|
||||
str += "Recoverable: "+ce.isRecoverable()+DELIMITER;
|
||||
str += "Transient: "+ce.isTransient()+DELIMITER;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
str += "Codec Error Code: "+ce.getErrorCode()+"\n";
|
||||
str += "Codec Error Code: "+ce.getErrorCode()+DELIMITER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
str += "/proc/cpuinfo:\n";
|
||||
try {
|
||||
str += MediaCodecHelper.readCpuinfo();
|
||||
} catch (Exception e) {
|
||||
str += e.getMessage();
|
||||
}
|
||||
|
||||
str += "Full decoder dump:\n";
|
||||
try {
|
||||
str += MediaCodecHelper.dumpDecoders();
|
||||
} catch (Exception e) {
|
||||
str += e.getMessage();
|
||||
}
|
||||
|
||||
str += originalException.toString();
|
||||
|
||||
return str;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.limelight.binding.video;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -14,6 +15,7 @@ import android.annotation.SuppressLint;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ConfigurationInfo;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||
@@ -31,7 +33,6 @@ public class MediaCodecHelper {
|
||||
private static final List<String> blacklistedDecoderPrefixes;
|
||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
|
||||
private static final List<String> deprioritizedHevcDecoders;
|
||||
private static final List<String> baselineProfileHackPrefixes;
|
||||
private static final List<String> directSubmitPrefixes;
|
||||
private static final List<String> constrainedHighProfilePrefixes;
|
||||
@@ -43,8 +44,10 @@ public class MediaCodecHelper {
|
||||
private static final List<String> kirinDecoderPrefixes;
|
||||
private static final List<String> exynosDecoderPrefixes;
|
||||
private static final List<String> amlogicDecoderPrefixes;
|
||||
private static final List<String> knownVendorLowLatencyOptions;
|
||||
|
||||
public static final boolean IS_EMULATOR = Build.HARDWARE.equals("ranchu") || Build.HARDWARE.equals("cheets");
|
||||
public static final boolean SHOULD_BYPASS_SOFTWARE_BLOCK =
|
||||
Build.HARDWARE.equals("ranchu") || Build.HARDWARE.equals("cheets") || Build.BRAND.equals("Android-x86");
|
||||
|
||||
private static boolean isLowEndSnapdragon = false;
|
||||
private static boolean isAdreno620 = false;
|
||||
@@ -70,7 +73,16 @@ public class MediaCodecHelper {
|
||||
|
||||
static {
|
||||
refFrameInvalidationAvcPrefixes = new LinkedList<>();
|
||||
|
||||
refFrameInvalidationHevcPrefixes = new LinkedList<>();
|
||||
refFrameInvalidationHevcPrefixes.add("omx.exynos");
|
||||
refFrameInvalidationHevcPrefixes.add("c2.exynos");
|
||||
|
||||
// The Chromecast with Google TV 4K works well with HEVC RFI since we also use the
|
||||
// vendor.low-latency.enable option.
|
||||
if (Build.DEVICE.equalsIgnoreCase("sabrina")) {
|
||||
refFrameInvalidationHevcPrefixes.add("omx.amlogic");
|
||||
}
|
||||
|
||||
// Qualcomm and NVIDIA may be added at runtime
|
||||
}
|
||||
@@ -82,18 +94,18 @@ public class MediaCodecHelper {
|
||||
static {
|
||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||
|
||||
// Blacklist software decoders that don't support H264 high profile,
|
||||
// but exclude the official AOSP and CrOS emulator from this restriction.
|
||||
if (!IS_EMULATOR) {
|
||||
// Blacklist software decoders that don't support H264 high profile except on systems
|
||||
// that are expected to only have software decoders (like emulators).
|
||||
if (!SHOULD_BYPASS_SOFTWARE_BLOCK) {
|
||||
blacklistedDecoderPrefixes.add("omx.google");
|
||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||
}
|
||||
|
||||
// We want to avoid ffmpeg decoders since they're software decoders,
|
||||
// but on Android-x86 they might be all we have (and also relatively
|
||||
// performant on a modern x86 processor).
|
||||
if (!Build.BRAND.equals("Android-x86")) {
|
||||
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
|
||||
// We want to avoid ffmpeg decoders since they're usually software decoders,
|
||||
// but we'll defer to the Android 10 isSoftwareOnly() API on newer devices
|
||||
// to determine if we should use these or not.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
|
||||
}
|
||||
}
|
||||
|
||||
// Force these decoders disabled because:
|
||||
@@ -194,14 +206,6 @@ public class MediaCodecHelper {
|
||||
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||
}
|
||||
|
||||
static {
|
||||
deprioritizedHevcDecoders = new LinkedList<>();
|
||||
|
||||
// These are decoders that work but aren't used by default for various reasons.
|
||||
|
||||
// Qualcomm is currently the only decoders in this group.
|
||||
}
|
||||
|
||||
static {
|
||||
useFourSlicesPrefixes = new LinkedList<>();
|
||||
|
||||
@@ -214,6 +218,15 @@ public class MediaCodecHelper {
|
||||
// Old Qualcomm decoders are detected at runtime
|
||||
}
|
||||
|
||||
static {
|
||||
knownVendorLowLatencyOptions = new LinkedList<>();
|
||||
|
||||
knownVendorLowLatencyOptions.add("vendor.qti-ext-dec-low-latency.enable");
|
||||
knownVendorLowLatencyOptions.add("vendor.hisi-ext-low-latency-video-dec.video-scene-for-low-latency-req");
|
||||
knownVendorLowLatencyOptions.add("vendor.rtc-ext-dec-low-latency.enable");
|
||||
knownVendorLowLatencyOptions.add("vendor.low-latency.enable");
|
||||
}
|
||||
|
||||
static {
|
||||
qualcommDecoderPrefixes = new LinkedList<>();
|
||||
|
||||
@@ -305,12 +318,33 @@ public class MediaCodecHelper {
|
||||
// We still have to check Build.MANUFACTURER to catch Amazon Fire tablets.
|
||||
if (context.getPackageManager().hasSystemFeature("amazon.hardware.fire_tv") ||
|
||||
Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||
// HEVC and RFI have been confirmed working on Fire TV 2, Fire TV Stick 2, Fire TV 4K Max,
|
||||
// Fire HD 8 2020, and Fire HD 8 2022 models.
|
||||
//
|
||||
// This is probably a good enough sample to conclude that all MediaTek Fire OS devices
|
||||
// are likely to be okay.
|
||||
whitelistedHevcDecoders.add("omx.mtk");
|
||||
refFrameInvalidationHevcPrefixes.add("omx.mtk");
|
||||
refFrameInvalidationHevcPrefixes.add("c2.mtk");
|
||||
|
||||
// This requires setting vdec-lowlatency on the Fire TV 3, otherwise the decoder
|
||||
// never produces any output frames. See comment above for details on why we only
|
||||
// do this for Fire TV devices.
|
||||
whitelistedHevcDecoders.add("omx.amlogic");
|
||||
|
||||
// Fire TV 3 seems to produce random artifacts on HEVC streams after packet loss.
|
||||
// Enabling RFI turns these artifacts into full decoder output hangs, so let's not enable
|
||||
// that for Fire OS 6 Amlogic devices. We will leave HEVC enabled because that's the only
|
||||
// way these devices can hit 4K. Hopefully this is just a problem with the BSP used in
|
||||
// the Fire OS 6 Amlogic devices, so we will leave this enabled for Fire OS 7+.
|
||||
//
|
||||
// Apart from a few TV models, the main Amlogic-based Fire TV devices are the Fire TV
|
||||
// Cubes and Fire TV 3. This check will exclude the Fire TV 3 and Fire TV Cube 1, but
|
||||
// allow the newer Fire TV Cubes to use HEVC RFI.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
refFrameInvalidationHevcPrefixes.add("omx.amlogic");
|
||||
refFrameInvalidationHevcPrefixes.add("c2.amlogic");
|
||||
}
|
||||
}
|
||||
|
||||
ActivityManager activityManager =
|
||||
@@ -324,19 +358,17 @@ public class MediaCodecHelper {
|
||||
|
||||
// Tegra K1 and later can do reference frame invalidation properly
|
||||
if (configInfo.reqGlEsVersion >= 0x30000) {
|
||||
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
||||
LimeLog.info("Added omx.nvidia/c2.nvidia to reference frame invalidation support list");
|
||||
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
||||
refFrameInvalidationHevcPrefixes.add("omx.nvidia");
|
||||
refFrameInvalidationAvcPrefixes.add("c2.nvidia"); // Unconfirmed
|
||||
refFrameInvalidationHevcPrefixes.add("c2.nvidia"); // Unconfirmed
|
||||
|
||||
LimeLog.info("Added omx.qcom/c2.qti to AVC reference frame invalidation support list");
|
||||
LimeLog.info("Added omx.qcom/c2.qti to reference frame invalidation support list");
|
||||
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
||||
refFrameInvalidationHevcPrefixes.add("omx.qcom");
|
||||
refFrameInvalidationAvcPrefixes.add("c2.qti");
|
||||
|
||||
// Prior to M, we were tricking the decoder into using baseline profile, which
|
||||
// won't support RFI properly.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
||||
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
||||
}
|
||||
refFrameInvalidationHevcPrefixes.add("c2.qti");
|
||||
}
|
||||
|
||||
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
||||
@@ -348,13 +380,9 @@ public class MediaCodecHelper {
|
||||
// (see comment on isGLES31SnapdragonRenderer).
|
||||
//
|
||||
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
||||
// We prefer reference frame invalidation support (which is only doable on AVC on
|
||||
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
||||
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
|
||||
// HEVC anyway.
|
||||
LimeLog.info("Added omx.qcom/c2.qti to deprioritized HEVC decoders based on GLES 3.1+ support");
|
||||
deprioritizedHevcDecoders.add("omx.qcom");
|
||||
deprioritizedHevcDecoders.add("c2.qti");
|
||||
LimeLog.info("Added omx.qcom/c2.qti to HEVC decoders based on GLES 3.1+ support");
|
||||
whitelistedHevcDecoders.add("omx.qcom");
|
||||
whitelistedHevcDecoders.add("c2.qti");
|
||||
}
|
||||
else {
|
||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
||||
@@ -374,8 +402,9 @@ public class MediaCodecHelper {
|
||||
// decoder hangs on the newer GE8100, GE8300, and GE8320 GPUs, so we limit it to the
|
||||
// Series6XT GPUs where we know it works.
|
||||
if (glRenderer.contains("GX6")) {
|
||||
LimeLog.info("Added omx.mtk to RFI list for HEVC");
|
||||
LimeLog.info("Added omx.mtk/c2.mtk to RFI list for HEVC");
|
||||
refFrameInvalidationHevcPrefixes.add("omx.mtk");
|
||||
refFrameInvalidationHevcPrefixes.add("c2.mtk");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,6 +445,35 @@ public class MediaCodecHelper {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean decoderSupportsKnownVendorLowLatencyOption(String decoderName) {
|
||||
// It's only possible to probe vendor parameters on Android 12 and above.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
MediaCodec testCodec = null;
|
||||
try {
|
||||
// Unfortunately we have to create an actual codec instance to get supported options.
|
||||
testCodec = MediaCodec.createByCodecName(decoderName);
|
||||
|
||||
// See if any of the vendor parameters match ones we know about
|
||||
for (String supportedOption : testCodec.getSupportedVendorParameters()) {
|
||||
for (String knownLowLatencyOption : knownVendorLowLatencyOptions) {
|
||||
if (supportedOption.equalsIgnoreCase(knownLowLatencyOption)) {
|
||||
LimeLog.info(decoderName + " supports known low latency option: " + supportedOption);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Tolerate buggy codecs
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (testCodec != null) {
|
||||
testCodec.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean decoderSupportsMaxOperatingRate(String decoderName) {
|
||||
// Operate at maximum rate to lower latency as much as possible on
|
||||
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
|
||||
@@ -476,6 +534,8 @@ public class MediaCodecHelper {
|
||||
// https://cs.android.com/android/_/android/platform/frameworks/av/+/01c10f8cdcd58d1e7025f426a72e6e75ba5d7fc2
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Try vendor-specific low latency options
|
||||
//
|
||||
// NOTE: Update knownVendorLowLatencyOptions if you modify this code!
|
||||
if (isDecoderInList(qualcommDecoderPrefixes, decoderInfo.getName())) {
|
||||
// Examples of Qualcomm's vendor extensions for Snapdragon 845:
|
||||
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/vdec/src/omx_vdec_extensions.hpp
|
||||
@@ -611,11 +671,24 @@ public class MediaCodecHelper {
|
||||
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) {
|
||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
||||
public static boolean decoderSupportsRefFrameInvalidationHevc(MediaCodecInfo decoderInfo) {
|
||||
// HEVC decoders seem to universally support RFI, but it can have huge latency penalties
|
||||
// for some decoders due to the number of references frames being > 1. Old Amlogic
|
||||
// decoders are known to have this problem.
|
||||
//
|
||||
// If the decoder supports FEATURE_LowLatency or any vendor low latency option,
|
||||
// we will use that as an indication that it can handle HEVC RFI without excessively
|
||||
// buffering frames.
|
||||
if (decoderSupportsAndroidRLowLatency(decoderInfo, "video/hevc") ||
|
||||
decoderSupportsKnownVendorLowLatencyOption(decoderInfo.getName())) {
|
||||
LimeLog.info("Enabling HEVC RFI based on low latency option support");
|
||||
return true;
|
||||
}
|
||||
|
||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderInfo.getName());
|
||||
}
|
||||
|
||||
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData, PreferenceConfiguration prefs) {
|
||||
public static boolean decoderIsWhitelistedForHevc(MediaCodecInfo decoderInfo) {
|
||||
// Google didn't have official support for HEVC (or more importantly, a CTS test) until
|
||||
// Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC,
|
||||
// so I'm restricting HEVC usage to Lollipop and higher.
|
||||
@@ -629,26 +702,43 @@ public class MediaCodecHelper {
|
||||
// OMX.qcom.video.decoder.hevcswvdec
|
||||
// OMX.SEC.hevc.sw.dec
|
||||
//
|
||||
if (decoderName.contains("sw")) {
|
||||
if (decoderInfo.getName().contains("sw")) {
|
||||
LimeLog.info("Disallowing HEVC on software decoder: " + decoderInfo.getName());
|
||||
return false;
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (!decoderInfo.isHardwareAccelerated() || decoderInfo.isSoftwareOnly())) {
|
||||
LimeLog.info("Disallowing HEVC on software decoder: " + decoderInfo.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some devices have HEVC decoders that we prefer not to use
|
||||
// typically because it can't support reference frame invalidation.
|
||||
// However, we will use it for HDR and for streaming over mobile networks
|
||||
// since it works fine otherwise. We will also use it for 4K because RFI
|
||||
// is currently disabled due to issues with video corruption.
|
||||
if (isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
||||
if (meteredData || (prefs.width == 3840 && prefs.height == 2160)) {
|
||||
LimeLog.info("Selected deprioritized decoder");
|
||||
// If this device is media performance class 12 or higher, we will assume any hardware
|
||||
// HEVC decoder present is fast and modern enough for streaming.
|
||||
//
|
||||
// [5.3/H-1-1] MUST NOT drop more than 2 frames in 10 seconds (i.e less than 0.333 percent frame drop) for a 1080p 60 fps video session under load.
|
||||
//
|
||||
// NB: We use reflection here because this field seems to be absent on Amazon Fire OS devices
|
||||
try {
|
||||
Field mediaClassField = Build.VERSION.class.getDeclaredField("MEDIA_PERFORMANCE_CLASS");
|
||||
int mediaClass = mediaClassField.getInt(null);
|
||||
if (mediaClass >= Build.VERSION_CODES.S) {
|
||||
LimeLog.info("Allowing HEVC based on media performance class: " + mediaClass);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
LimeLog.info("Build.VERSION.MEDIA_PERFORMANCE_CLASS not present");
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
||||
// If the decoder supports FEATURE_LowLatency, we will assume it is fast and modern enough
|
||||
// to be preferable for streaming over H.264 decoders.
|
||||
if (decoderSupportsAndroidRLowLatency(decoderInfo, "video/hevc")) {
|
||||
LimeLog.info("Allowing HEVC based on FEATURE_LowLatency support");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, we use our list of known working HEVC decoders
|
||||
return isDecoderInList(whitelistedHevcDecoders, decoderInfo.getName());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@@ -721,7 +811,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;
|
||||
}
|
||||
@@ -852,8 +942,7 @@ public class MediaCodecHelper {
|
||||
|
||||
public static String readCpuinfo() throws Exception {
|
||||
StringBuilder cpuInfo = new StringBuilder();
|
||||
BufferedReader br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
|
||||
try {
|
||||
try (final BufferedReader br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")))) {
|
||||
for (;;) {
|
||||
int ch = br.read();
|
||||
if (ch == -1)
|
||||
@@ -862,8 +951,6 @@ public class MediaCodecHelper {
|
||||
}
|
||||
|
||||
return cpuInfo.toString();
|
||||
} finally {
|
||||
br.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
@@ -27,6 +28,7 @@ public class ComputerDatabaseManager {
|
||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||
|
||||
private static final char ADDRESS_DELIMITER = ';';
|
||||
private static final char PORT_DELIMITER = '_';
|
||||
|
||||
private SQLiteDatabase computerDb;
|
||||
|
||||
@@ -74,10 +76,10 @@ public class ComputerDatabaseManager {
|
||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||
|
||||
StringBuilder addresses = new StringBuilder();
|
||||
addresses.append(details.localAddress != null ? details.localAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? details.remoteAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? details.manualAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? details.ipv6Address : "");
|
||||
addresses.append(details.localAddress != null ? splitTupleToAddress(details.localAddress) : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? splitTupleToAddress(details.remoteAddress) : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? splitTupleToAddress(details.manualAddress) : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? splitTupleToAddress(details.ipv6Address) : "");
|
||||
|
||||
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
|
||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
||||
@@ -103,6 +105,24 @@ public class ComputerDatabaseManager {
|
||||
return input;
|
||||
}
|
||||
|
||||
private static ComputerDetails.AddressTuple splitAddressToTuple(String input) {
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] parts = input.split(""+PORT_DELIMITER, -1);
|
||||
if (parts.length == 1) {
|
||||
return new ComputerDetails.AddressTuple(parts[0], NvHTTP.DEFAULT_HTTP_PORT);
|
||||
}
|
||||
else {
|
||||
return new ComputerDetails.AddressTuple(parts[0], Integer.parseInt(parts[1]));
|
||||
}
|
||||
}
|
||||
|
||||
private static String splitTupleToAddress(ComputerDetails.AddressTuple tuple) {
|
||||
return tuple.address+PORT_DELIMITER+tuple.port;
|
||||
}
|
||||
|
||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
@@ -111,10 +131,18 @@ public class ComputerDatabaseManager {
|
||||
|
||||
String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1);
|
||||
|
||||
details.localAddress = readNonEmptyString(addresses[0]);
|
||||
details.remoteAddress = readNonEmptyString(addresses[1]);
|
||||
details.manualAddress = readNonEmptyString(addresses[2]);
|
||||
details.ipv6Address = readNonEmptyString(addresses[3]);
|
||||
details.localAddress = splitAddressToTuple(readNonEmptyString(addresses[0]));
|
||||
details.remoteAddress = splitAddressToTuple(readNonEmptyString(addresses[1]));
|
||||
details.manualAddress = splitAddressToTuple(readNonEmptyString(addresses[2]));
|
||||
details.ipv6Address = splitAddressToTuple(readNonEmptyString(addresses[3]));
|
||||
|
||||
// External port is persisted in the remote address field
|
||||
if (details.remoteAddress != null) {
|
||||
details.externalPort = details.remoteAddress.port;
|
||||
}
|
||||
else {
|
||||
details.externalPort = NvHTTP.DEFAULT_HTTP_PORT;
|
||||
}
|
||||
|
||||
details.macAddress = c.getString(3);
|
||||
|
||||
@@ -136,28 +164,26 @@ public class ComputerDatabaseManager {
|
||||
}
|
||||
|
||||
public List<ComputerDetails> getAllComputers() {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
computerList.add(getComputerFromCursor(c));
|
||||
try (final Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null)) {
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
computerList.add(getComputerFromCursor(c));
|
||||
}
|
||||
return computerList;
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerByUUID(String uuid) {
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
c.close();
|
||||
return null;
|
||||
try (final Cursor c = computerDb.query(
|
||||
COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?",
|
||||
new String[]{ uuid }, null, null, null)
|
||||
) {
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
return null;
|
||||
}
|
||||
|
||||
return getComputerFromCursor(c);
|
||||
}
|
||||
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
c.close();
|
||||
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ public class ComputerManagerService extends Service {
|
||||
// then use STUN to populate the external address field if
|
||||
// it's not set already.
|
||||
if (details.remoteAddress == null) {
|
||||
InetAddress addr = InetAddress.getByName(details.activeAddress);
|
||||
InetAddress addr = InetAddress.getByName(details.activeAddress.address);
|
||||
if (addr.isSiteLocalAddress()) {
|
||||
populateExternalAddress(details);
|
||||
}
|
||||
@@ -369,7 +369,12 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
// Perform the STUN request if we're not on a VPN or if we bound to a network
|
||||
if (!activeNetworkIsVpn || boundToNetwork) {
|
||||
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||
String stunResolvedAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||
if (stunResolvedAddress != null) {
|
||||
// We don't know for sure what the external port is, so we will have to guess.
|
||||
// When we contact the PC (if we haven't already), it will update the port.
|
||||
details.remoteAddress = new ComputerDetails.AddressTuple(stunResolvedAddress, details.guessExternalPort());
|
||||
}
|
||||
}
|
||||
|
||||
// Unbind from the network
|
||||
@@ -396,7 +401,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
// Populate the computer template with mDNS info
|
||||
if (computer.getLocalAddress() != null) {
|
||||
details.localAddress = computer.getLocalAddress().getHostAddress();
|
||||
details.localAddress = new ComputerDetails.AddressTuple(computer.getLocalAddress().getHostAddress(), computer.getPort());
|
||||
|
||||
// Since we're on the same network, we can use STUN to find
|
||||
// our WAN address, which is also very likely the WAN address
|
||||
@@ -406,7 +411,7 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
if (computer.getIpv6Address() != null) {
|
||||
details.ipv6Address = computer.getIpv6Address().getHostAddress();
|
||||
details.ipv6Address = new ComputerDetails.AddressTuple(computer.getIpv6Address().getHostAddress(), computer.getPort());
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -543,12 +548,20 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
|
||||
private ComputerDetails tryPollIp(ComputerDetails details, ComputerDetails.AddressTuple address) {
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
||||
// If the current address's port number matches the active address's port number, we can also assume
|
||||
// the HTTPS port will also match. This assumption is currently safe because Sunshine sets all ports
|
||||
// as offsets from the base HTTP port and doesn't allow custom HttpsPort responses for WAN vs LAN.
|
||||
boolean portMatchesActiveAddress = details.activeAddress != null && address.port == details.activeAddress.port;
|
||||
|
||||
NvHTTP http = new NvHTTP(address, portMatchesActiveAddress ? details.httpsPort : 0, idManager.getUniqueId(), details.serverCert,
|
||||
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
ComputerDetails newDetails = http.getComputerDetails();
|
||||
// If this PC is currently online at this address, extend the timeouts to allow more time for the PC to respond.
|
||||
boolean isLikelyOnline = details.state == ComputerDetails.State.ONLINE && address.equals(details.activeAddress);
|
||||
|
||||
ComputerDetails newDetails = http.getComputerDetails(isLikelyOnline);
|
||||
|
||||
// Check if this is the PC we expected
|
||||
if (newDetails.uuid == null) {
|
||||
@@ -572,14 +585,14 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
private static class ParallelPollTuple {
|
||||
public String address;
|
||||
public ComputerDetails.AddressTuple address;
|
||||
public ComputerDetails existingDetails;
|
||||
|
||||
public boolean complete;
|
||||
public Thread pollingThread;
|
||||
public ComputerDetails returnedDetails;
|
||||
|
||||
public ParallelPollTuple(String address, ComputerDetails existingDetails) {
|
||||
public ParallelPollTuple(ComputerDetails.AddressTuple address, ComputerDetails existingDetails) {
|
||||
this.address = address;
|
||||
this.existingDetails = existingDetails;
|
||||
}
|
||||
@@ -591,7 +604,7 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private void startParallelPollThread(ParallelPollTuple tuple, HashSet<String> uniqueAddresses) {
|
||||
private void startParallelPollThread(ParallelPollTuple tuple, HashSet<ComputerDetails.AddressTuple> uniqueAddresses) {
|
||||
// Don't bother starting a polling thread for an address that doesn't exist
|
||||
// or if the address has already been polled with an earlier tuple
|
||||
if (tuple.address == null || !uniqueAddresses.add(tuple.address)) {
|
||||
@@ -625,7 +638,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
// These must be started in order of precedence for the deduplication algorithm
|
||||
// to result in the correct behavior.
|
||||
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||
HashSet<ComputerDetails.AddressTuple> uniqueAddresses = new HashSet<>();
|
||||
startParallelPollThread(localInfo, uniqueAddresses);
|
||||
startParallelPollThread(manualInfo, uniqueAddresses);
|
||||
startParallelPollThread(remoteInfo, uniqueAddresses);
|
||||
@@ -821,7 +834,7 @@ public class ComputerManagerService extends Service {
|
||||
PollingTuple tuple = getPollingTuple(computer);
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), computer.httpsPort, idManager.getUniqueId(),
|
||||
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
String appList;
|
||||
@@ -849,18 +862,12 @@ public class ComputerManagerService extends Service {
|
||||
if (!appList.isEmpty() &&
|
||||
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||
// Open the cache file
|
||||
OutputStream cacheOut = null;
|
||||
try {
|
||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
|
||||
try (final OutputStream cacheOut = CacheHelper.openCacheFileForOutput(
|
||||
getCacheDir(), "applist", computer.uuid)
|
||||
) {
|
||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (cacheOut != null) {
|
||||
cacheOut.close();
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
// Reset empty count if it wasn't empty this time
|
||||
|
||||
@@ -33,12 +33,11 @@ public class IdentityManager {
|
||||
private static String loadUniqueId(Context c) {
|
||||
// 2 Hex digits per byte
|
||||
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
||||
InputStreamReader reader = null;
|
||||
LimeLog.info("Reading UID from disk");
|
||||
try {
|
||||
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
||||
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
||||
{
|
||||
try (final InputStreamReader reader =
|
||||
new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME))
|
||||
) {
|
||||
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2) {
|
||||
LimeLog.severe("UID file data is truncated");
|
||||
return null;
|
||||
}
|
||||
@@ -50,12 +49,6 @@ public class IdentityManager {
|
||||
LimeLog.severe("Error while reading UID file");
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,20 +57,14 @@ public class IdentityManager {
|
||||
LimeLog.info("Generating new UID");
|
||||
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
||||
|
||||
OutputStreamWriter writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
||||
try (final OutputStreamWriter writer =
|
||||
new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0))
|
||||
) {
|
||||
writer.write(uidStr);
|
||||
LimeLog.info("UID written to disk");
|
||||
} catch (IOException e) {
|
||||
LimeLog.severe("Error while writing UID file");
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// We can return a UID even if I/O fails
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
@@ -30,26 +31,26 @@ public class LegacyDatabaseReader {
|
||||
// too. To disambiguate, we'll need to prefix them with a string
|
||||
// greater than the allowable IP address length.
|
||||
try {
|
||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
||||
details.localAddress = new ComputerDetails.AddressTuple(InetAddress.getByAddress(c.getBlob(2)).getHostAddress(), NvHTTP.DEFAULT_HTTP_PORT);
|
||||
LimeLog.warning("DB: Legacy local address for " + details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(2);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
||||
details.localAddress = new ComputerDetails.AddressTuple(c.getString(2).substring(ADDRESS_PREFIX.length()), NvHTTP.DEFAULT_HTTP_PORT);
|
||||
} else {
|
||||
LimeLog.severe("DB: Corrupted local address for " + details.name);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
||||
details.remoteAddress = new ComputerDetails.AddressTuple(InetAddress.getByAddress(c.getBlob(3)).getHostAddress(), NvHTTP.DEFAULT_HTTP_PORT);
|
||||
LimeLog.warning("DB: Legacy remote address for " + details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(3);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
||||
details.remoteAddress = new ComputerDetails.AddressTuple(c.getString(3).substring(ADDRESS_PREFIX.length()), NvHTTP.DEFAULT_HTTP_PORT);
|
||||
} else {
|
||||
LimeLog.severe("DB: Corrupted remote address for " + details.name);
|
||||
}
|
||||
@@ -68,37 +69,34 @@ public class LegacyDatabaseReader {
|
||||
}
|
||||
|
||||
private static List<ComputerDetails> getAllComputers(SQLiteDatabase db) {
|
||||
Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
try (final Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null)) {
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
return computerList;
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
||||
SQLiteDatabase computerDb = null;
|
||||
try {
|
||||
try (final SQLiteDatabase computerDb = SQLiteDatabase.openDatabase(
|
||||
c.getDatabasePath(COMPUTER_DB_NAME).getPath(),
|
||||
null, SQLiteDatabase.OPEN_READONLY)
|
||||
) {
|
||||
// Open the existing database
|
||||
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||
return getAllComputers(computerDb);
|
||||
} catch (SQLiteException e) {
|
||||
return new LinkedList<ComputerDetails>();
|
||||
} finally {
|
||||
// Close and delete the old DB
|
||||
if (computerDb != null) {
|
||||
computerDb.close();
|
||||
}
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateException;
|
||||
@@ -23,9 +24,9 @@ public class LegacyDatabaseReader2 {
|
||||
|
||||
details.uuid = c.getString(0);
|
||||
details.name = c.getString(1);
|
||||
details.localAddress = c.getString(2);
|
||||
details.remoteAddress = c.getString(3);
|
||||
details.manualAddress = c.getString(4);
|
||||
details.localAddress = new ComputerDetails.AddressTuple(c.getString(2), NvHTTP.DEFAULT_HTTP_PORT);
|
||||
details.remoteAddress = new ComputerDetails.AddressTuple(c.getString(3), NvHTTP.DEFAULT_HTTP_PORT);
|
||||
details.manualAddress = new ComputerDetails.AddressTuple(c.getString(4), NvHTTP.DEFAULT_HTTP_PORT);
|
||||
details.macAddress = c.getString(5);
|
||||
|
||||
// This column wasn't always present in the old schema
|
||||
@@ -49,37 +50,34 @@ public class LegacyDatabaseReader2 {
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
try (final Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null)) {
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
return computerList;
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
||||
SQLiteDatabase computerDb = null;
|
||||
try {
|
||||
try (final SQLiteDatabase computerDb = SQLiteDatabase.openDatabase(
|
||||
c.getDatabasePath(COMPUTER_DB_NAME).getPath(),
|
||||
null, SQLiteDatabase.OPEN_READONLY)
|
||||
) {
|
||||
// Open the existing database
|
||||
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||
return getAllComputers(computerDb);
|
||||
} catch (SQLiteException e) {
|
||||
return new LinkedList<ComputerDetails>();
|
||||
} finally {
|
||||
// Close and delete the old DB
|
||||
if (computerDb != null) {
|
||||
computerDb.close();
|
||||
}
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,21 +154,15 @@ public class DiskAssetLoader {
|
||||
}
|
||||
|
||||
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||
OutputStream out = null;
|
||||
boolean success = false;
|
||||
try {
|
||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
try (final OutputStream out = CacheHelper.openCacheFileForOutput(
|
||||
cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png")
|
||||
) {
|
||||
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||
success = true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (out != null) {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
|
||||
@@ -22,8 +22,9 @@ public class NetworkAssetLoader {
|
||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
InputStream in = null;
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
|
||||
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer),
|
||||
tuple.computer.httpsPort, uniqueId, tuple.computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(context));
|
||||
in = http.getBoxArt(tuple.app);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class ConnectionContext {
|
||||
public String serverAddress;
|
||||
public ComputerDetails.AddressTuple serverAddress;
|
||||
public int httpsPort;
|
||||
public X509Certificate serverCert;
|
||||
public StreamConfiguration streamConfig;
|
||||
public NvConnectionListener connListener;
|
||||
@@ -22,5 +25,8 @@ public class ConnectionContext {
|
||||
public int negotiatedWidth, negotiatedHeight;
|
||||
public boolean negotiatedHdr;
|
||||
|
||||
public int negotiatedRemoteStreaming;
|
||||
public int negotiatedPacketSize;
|
||||
|
||||
public int videoCapabilities;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.IpPrefix;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.RouteInfo;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
@@ -19,6 +32,7 @@ import org.xmlpull.v1.XmlPullParserException;
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
@@ -29,13 +43,13 @@ import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class NvConnection {
|
||||
// Context parameters
|
||||
private String host;
|
||||
private LimelightCryptoProvider cryptoProvider;
|
||||
private String uniqueId;
|
||||
private ConnectionContext context;
|
||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||
private final boolean isMonkey;
|
||||
private final boolean batchMouseInput;
|
||||
private final Context appContext;
|
||||
|
||||
private static final int MOUSE_BATCH_PERIOD_MS = 5;
|
||||
private Timer mouseInputTimer;
|
||||
@@ -43,20 +57,22 @@ public class NvConnection {
|
||||
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;
|
||||
public NvConnection(Context appContext, ComputerDetails.AddressTuple host, int httpsPort, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert, boolean batchMouseInput)
|
||||
{
|
||||
this.appContext = appContext;
|
||||
this.cryptoProvider = cryptoProvider;
|
||||
this.uniqueId = uniqueId;
|
||||
this.batchMouseInput = batchMouseInput;
|
||||
|
||||
this.context = new ConnectionContext();
|
||||
this.context.serverAddress = host;
|
||||
this.context.httpsPort = httpsPort;
|
||||
this.context.streamConfig = config;
|
||||
this.context.serverCert = serverCert;
|
||||
|
||||
// This is unique per connection
|
||||
this.context.riKey = generateRiAesKey();
|
||||
context.riKeyId = generateRiKeyId();
|
||||
this.context.riKeyId = generateRiKeyId();
|
||||
|
||||
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||
}
|
||||
@@ -116,12 +132,130 @@ public class NvConnection {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private InetAddress resolveServerAddress() throws IOException {
|
||||
// Try to find an address that works for this host
|
||||
InetAddress[] addrs = InetAddress.getAllByName(context.serverAddress.address);
|
||||
for (InetAddress addr : addrs) {
|
||||
try (Socket s = new Socket()) {
|
||||
s.setSoLinger(true, 0);
|
||||
s.connect(new InetSocketAddress(addr, context.serverAddress.port), 1000);
|
||||
return addr;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// If we made it here, we didn't manage to find a working address. If DNS returned any
|
||||
// address, we'll use the first available address and hope for the best.
|
||||
if (addrs.length > 0) {
|
||||
return addrs[0];
|
||||
}
|
||||
else {
|
||||
throw new IOException("No addresses found for "+context.serverAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private int detectServerConnectionType() {
|
||||
ConnectivityManager connMgr = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Network activeNetwork = connMgr.getActiveNetwork();
|
||||
if (activeNetwork != null) {
|
||||
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
|
||||
if (netCaps != null) {
|
||||
if (netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
|
||||
// VPNs are treated as remote connections
|
||||
return StreamConfiguration.STREAM_CFG_REMOTE;
|
||||
}
|
||||
else if (netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||
// Cellular is always treated as remote to avoid any possible
|
||||
// issues with 464XLAT or similar technologies.
|
||||
return StreamConfiguration.STREAM_CFG_REMOTE;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the server address is on-link
|
||||
LinkProperties linkProperties = connMgr.getLinkProperties(activeNetwork);
|
||||
if (linkProperties != null) {
|
||||
InetAddress serverAddress;
|
||||
try {
|
||||
serverAddress = resolveServerAddress();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
// We can't decide without being able to resolve the server address
|
||||
return StreamConfiguration.STREAM_CFG_AUTO;
|
||||
}
|
||||
|
||||
// If the address is in the NAT64 prefix, always treat it as remote
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
IpPrefix nat64Prefix = linkProperties.getNat64Prefix();
|
||||
if (nat64Prefix != null && nat64Prefix.contains(serverAddress)) {
|
||||
return StreamConfiguration.STREAM_CFG_REMOTE;
|
||||
}
|
||||
}
|
||||
|
||||
for (RouteInfo route : linkProperties.getRoutes()) {
|
||||
// Skip non-unicast routes (which are all we get prior to Android 13)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && route.getType() != RouteInfo.RTN_UNICAST) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the first route that matches this address
|
||||
if (route.matches(serverAddress)) {
|
||||
// If there's no gateway, this is an on-link destination
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// We want to use hasGateway() because getGateway() doesn't adhere
|
||||
// to documented behavior of returning null for on-link addresses.
|
||||
if (!route.hasGateway()) {
|
||||
return StreamConfiguration.STREAM_CFG_LOCAL;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// getGateway() is documented to return null for on-link destinations,
|
||||
// but it actually returns the unspecified address (0.0.0.0 or ::).
|
||||
InetAddress gateway = route.getGateway();
|
||||
if (gateway == null || gateway.isAnyLocalAddress()) {
|
||||
return StreamConfiguration.STREAM_CFG_LOCAL;
|
||||
}
|
||||
}
|
||||
|
||||
// We _should_ stop after the first matching route, but for some reason
|
||||
// Android doesn't always report IPv6 routes in descending order of
|
||||
// specificity and metric. To handle that case, we enumerate all matching
|
||||
// routes, assuming that an on-link route will always be preferred.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||
if (activeNetworkInfo != null) {
|
||||
switch (activeNetworkInfo.getType()) {
|
||||
case ConnectivityManager.TYPE_VPN:
|
||||
case ConnectivityManager.TYPE_MOBILE:
|
||||
case ConnectivityManager.TYPE_MOBILE_DUN:
|
||||
case ConnectivityManager.TYPE_MOBILE_HIPRI:
|
||||
case ConnectivityManager.TYPE_MOBILE_MMS:
|
||||
case ConnectivityManager.TYPE_MOBILE_SUPL:
|
||||
case ConnectivityManager.TYPE_WIMAX:
|
||||
// VPNs and cellular connections are always remote connections
|
||||
return StreamConfiguration.STREAM_CFG_REMOTE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't determine the connection type, let moonlight-common-c decide.
|
||||
return StreamConfiguration.STREAM_CFG_AUTO;
|
||||
}
|
||||
|
||||
private boolean startApp() throws XmlPullParserException, IOException
|
||||
{
|
||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||
NvHTTP h = new NvHTTP(context.serverAddress, context.httpsPort, uniqueId, context.serverCert, cryptoProvider);
|
||||
|
||||
String serverInfo = h.getServerInfo();
|
||||
String serverInfo = h.getServerInfo(true);
|
||||
|
||||
context.serverAppVersion = h.getServerVersion(serverInfo);
|
||||
if (context.serverAppVersion == null) {
|
||||
@@ -171,6 +305,18 @@ public class NvConnection {
|
||||
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||
}
|
||||
|
||||
// We will perform some connection type detection if the caller asked for it
|
||||
if (context.streamConfig.getRemote() == StreamConfiguration.STREAM_CFG_AUTO) {
|
||||
context.negotiatedRemoteStreaming = detectServerConnectionType();
|
||||
context.negotiatedPacketSize =
|
||||
context.negotiatedRemoteStreaming == StreamConfiguration.STREAM_CFG_REMOTE ?
|
||||
1024 : context.streamConfig.getMaxPacketSize();
|
||||
}
|
||||
else {
|
||||
context.negotiatedRemoteStreaming = context.streamConfig.getRemote();
|
||||
context.negotiatedPacketSize = context.streamConfig.getMaxPacketSize();
|
||||
}
|
||||
|
||||
//
|
||||
// Video stream format will be decided during the RTSP handshake
|
||||
@@ -269,7 +415,6 @@ public class NvConnection {
|
||||
|
||||
String appName = context.streamConfig.getApp().getAppName();
|
||||
|
||||
context.serverAddress = host;
|
||||
context.connListener.stageStarting(appName);
|
||||
|
||||
try {
|
||||
@@ -307,19 +452,21 @@ public class NvConnection {
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||
int ret = MoonBridge.startConnection(context.serverAddress.address,
|
||||
context.serverAppVersion, context.serverGfeVersion, context.rtspSessionUrl,
|
||||
context.negotiatedWidth, context.negotiatedHeight,
|
||||
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
||||
context.streamConfig.getMaxPacketSize(),
|
||||
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration().toInt(),
|
||||
context.negotiatedPacketSize, context.negotiatedRemoteStreaming,
|
||||
context.streamConfig.getAudioConfiguration().toInt(),
|
||||
context.streamConfig.getHevcSupported(),
|
||||
context.negotiatedHdr,
|
||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||
context.streamConfig.getClientRefreshRateX100(),
|
||||
context.streamConfig.getEncryptionFlags(),
|
||||
context.riKey.getEncoded(), ib.array(),
|
||||
context.videoCapabilities);
|
||||
context.videoCapabilities,
|
||||
context.streamConfig.getColorSpace(),
|
||||
context.streamConfig.getColorRange());
|
||||
if (ret != 0) {
|
||||
// LiStartConnection() failed, so the caller is not expected
|
||||
// to stop the connection themselves. We need to release their
|
||||
|
||||
@@ -27,6 +27,8 @@ public class StreamConfiguration {
|
||||
private boolean enableHdr;
|
||||
private int attachedGamepadMask;
|
||||
private int encryptionFlags;
|
||||
private int colorRange;
|
||||
private int colorSpace;
|
||||
|
||||
public static class Builder {
|
||||
private StreamConfiguration config = new StreamConfiguration();
|
||||
@@ -131,7 +133,17 @@ public class StreamConfiguration {
|
||||
config.supportsHevc = supportsHevc;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public StreamConfiguration.Builder setColorRange(int colorRange) {
|
||||
config.colorRange = colorRange;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setColorSpace(int colorSpace) {
|
||||
config.colorSpace = colorSpace;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration build() {
|
||||
return config;
|
||||
}
|
||||
@@ -226,4 +238,12 @@ public class StreamConfiguration {
|
||||
public int getEncryptionFlags() {
|
||||
return encryptionFlags;
|
||||
}
|
||||
|
||||
public int getColorRange() {
|
||||
return colorRange;
|
||||
}
|
||||
|
||||
public int getColorSpace() {
|
||||
return colorSpace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,57 @@ public class ComputerDetails {
|
||||
ONLINE, OFFLINE, UNKNOWN
|
||||
}
|
||||
|
||||
public static class AddressTuple {
|
||||
public String address;
|
||||
public int port;
|
||||
|
||||
public AddressTuple(String address, int port) {
|
||||
if (address == null) {
|
||||
throw new IllegalArgumentException("Address cannot be null");
|
||||
}
|
||||
if (port <= 0) {
|
||||
throw new IllegalArgumentException("Invalid port");
|
||||
}
|
||||
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof AddressTuple)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AddressTuple that = (AddressTuple) obj;
|
||||
return address.equals(that.address) && port == that.port;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return address + ":" + port;
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent attributes
|
||||
public String uuid;
|
||||
public String name;
|
||||
public String localAddress;
|
||||
public String remoteAddress;
|
||||
public String manualAddress;
|
||||
public String ipv6Address;
|
||||
public AddressTuple localAddress;
|
||||
public AddressTuple remoteAddress;
|
||||
public AddressTuple manualAddress;
|
||||
public AddressTuple ipv6Address;
|
||||
public String macAddress;
|
||||
public X509Certificate serverCert;
|
||||
|
||||
// Transient attributes
|
||||
public State state;
|
||||
public String activeAddress;
|
||||
public AddressTuple activeAddress;
|
||||
public int httpsPort;
|
||||
public int externalPort;
|
||||
public PairingManager.PairState pairState;
|
||||
public int runningGameId;
|
||||
public String rawAppList;
|
||||
@@ -35,6 +73,27 @@ public class ComputerDetails {
|
||||
update(details);
|
||||
}
|
||||
|
||||
public int guessExternalPort() {
|
||||
if (externalPort != 0) {
|
||||
return externalPort;
|
||||
}
|
||||
else if (remoteAddress != null) {
|
||||
return remoteAddress.port;
|
||||
}
|
||||
else if (activeAddress != null) {
|
||||
return activeAddress.port;
|
||||
}
|
||||
else if (ipv6Address != null) {
|
||||
return ipv6Address.port;
|
||||
}
|
||||
else if (localAddress != null) {
|
||||
return localAddress.port;
|
||||
}
|
||||
else {
|
||||
return NvHTTP.DEFAULT_HTTP_PORT;
|
||||
}
|
||||
}
|
||||
|
||||
public void update(ComputerDetails details) {
|
||||
this.state = details.state;
|
||||
this.name = details.name;
|
||||
@@ -43,12 +102,18 @@ public class ComputerDetails {
|
||||
this.activeAddress = details.activeAddress;
|
||||
}
|
||||
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
||||
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
||||
if (details.localAddress != null && !details.localAddress.address.startsWith("127.")) {
|
||||
this.localAddress = details.localAddress;
|
||||
}
|
||||
if (details.remoteAddress != null) {
|
||||
this.remoteAddress = details.remoteAddress;
|
||||
}
|
||||
else if (this.remoteAddress != null && details.externalPort != 0) {
|
||||
// If we have a remote address already (perhaps via STUN) but our updated details
|
||||
// don't have a new one (because GFE doesn't send one), propagate the external
|
||||
// port to the current remote address. We may have tried to guess it previously.
|
||||
this.remoteAddress.port = details.externalPort;
|
||||
}
|
||||
if (details.manualAddress != null) {
|
||||
this.manualAddress = details.manualAddress;
|
||||
}
|
||||
@@ -61,6 +126,8 @@ public class ComputerDetails {
|
||||
if (details.serverCert != null) {
|
||||
this.serverCert = details.serverCert;
|
||||
}
|
||||
this.externalPort = details.externalPort;
|
||||
this.httpsPort = details.httpsPort;
|
||||
this.pairState = details.pairState;
|
||||
this.runningGameId = details.runningGameId;
|
||||
this.rawAppList = details.rawAppList;
|
||||
@@ -80,6 +147,7 @@ public class ComputerDetails {
|
||||
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||
str.append("Pair State: ").append(pairState).append("\n");
|
||||
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||
str.append("HTTPS Port: ").append(httpsPort).append("\n");
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,19 +62,22 @@ public class NvHTTP {
|
||||
private String uniqueId;
|
||||
private PairingManager pm;
|
||||
|
||||
public static final int HTTPS_PORT = 47984;
|
||||
public static final int HTTP_PORT = 47989;
|
||||
public static final int CONNECTION_TIMEOUT = 3000;
|
||||
public static final int READ_TIMEOUT = 5000;
|
||||
private static final int DEFAULT_HTTPS_PORT = 47984;
|
||||
public static final int DEFAULT_HTTP_PORT = 47989;
|
||||
public static final int SHORT_CONNECTION_TIMEOUT = 3000;
|
||||
public static final int LONG_CONNECTION_TIMEOUT = 5000;
|
||||
public static final int READ_TIMEOUT = 7000;
|
||||
|
||||
// Print URL and content to logcat on debug builds
|
||||
private static boolean verbose = BuildConfig.DEBUG;
|
||||
|
||||
private HttpUrl baseUrlHttps;
|
||||
private HttpUrl baseUrlHttp;
|
||||
|
||||
private int httpsPort;
|
||||
|
||||
private OkHttpClient httpClient;
|
||||
private OkHttpClient httpClientWithReadTimeout;
|
||||
private OkHttpClient httpClientLongConnectTimeout;
|
||||
private OkHttpClient httpClientLongConnectNoReadTimeout;
|
||||
private OkHttpClient httpClientShortConnectTimeout;
|
||||
|
||||
private X509TrustManager defaultTrustManager;
|
||||
private X509TrustManager trustManager;
|
||||
@@ -167,20 +170,34 @@ public class NvHTTP {
|
||||
}
|
||||
};
|
||||
|
||||
httpClient = new OkHttpClient.Builder()
|
||||
httpClientLongConnectTimeout = new OkHttpClient.Builder()
|
||||
.connectionPool(new ConnectionPool(0, 1, TimeUnit.MILLISECONDS))
|
||||
.hostnameVerifier(hv)
|
||||
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.connectTimeout(LONG_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.proxy(Proxy.NO_PROXY)
|
||||
.build();
|
||||
|
||||
httpClientWithReadTimeout = httpClient.newBuilder()
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
|
||||
httpClientShortConnectTimeout = httpClientLongConnectTimeout.newBuilder()
|
||||
.connectTimeout(SHORT_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
httpClientLongConnectNoReadTimeout = httpClientLongConnectTimeout.newBuilder()
|
||||
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
public HttpUrl getHttpsUrl(boolean likelyOnline) throws IOException {
|
||||
if (httpsPort == 0) {
|
||||
// Fetch the HTTPS port if we don't have it already
|
||||
httpsPort = getHttpsPort(openHttpConnectionToString(likelyOnline ? httpClientLongConnectTimeout : httpClientShortConnectTimeout,
|
||||
baseUrlHttp, "serverinfo"));
|
||||
}
|
||||
|
||||
return new HttpUrl.Builder().scheme("https").host(baseUrlHttp.host()).port(httpsPort).build();
|
||||
}
|
||||
|
||||
public NvHTTP(String address, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException {
|
||||
public NvHTTP(ComputerDetails.AddressTuple address, int httpsPort, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException {
|
||||
// Use the same UID for all Moonlight clients so we can quit games
|
||||
// started by other Moonlight clients.
|
||||
this.uniqueId = "0123456789ABCDEF";
|
||||
@@ -189,17 +206,13 @@ public class NvHTTP {
|
||||
|
||||
initializeHttpState(cryptoProvider);
|
||||
|
||||
this.httpsPort = httpsPort;
|
||||
|
||||
try {
|
||||
this.baseUrlHttp = new HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(address)
|
||||
.port(HTTP_PORT)
|
||||
.build();
|
||||
|
||||
this.baseUrlHttps = new HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host(address)
|
||||
.port(HTTPS_PORT)
|
||||
.host(address.address)
|
||||
.port(address.port)
|
||||
.build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Encapsulate IllegalArgumentException into IOException for callers to handle more easily
|
||||
@@ -271,8 +284,11 @@ public class NvHTTP {
|
||||
}
|
||||
}
|
||||
|
||||
public String getServerInfo() throws IOException, XmlPullParserException {
|
||||
public String getServerInfo(boolean likelyOnline) throws IOException, XmlPullParserException {
|
||||
String resp;
|
||||
|
||||
// If we believe the PC is online, give it a little extra time to respond
|
||||
OkHttpClient client = likelyOnline ? httpClientLongConnectTimeout : httpClientShortConnectTimeout;
|
||||
|
||||
//
|
||||
// TODO: Shield Hub uses HTTP for this and is able to get an accurate PairStatus with HTTP.
|
||||
@@ -284,7 +300,7 @@ public class NvHTTP {
|
||||
if (serverCert != null) {
|
||||
try {
|
||||
try {
|
||||
resp = openHttpConnectionToString(baseUrlHttps, "serverinfo", true);
|
||||
resp = openHttpConnectionToString(client, getHttpsUrl(likelyOnline), "serverinfo");
|
||||
} catch (SSLHandshakeException e) {
|
||||
// Detect if we failed due to a server cert mismatch
|
||||
if (e.getCause() instanceof CertificateException) {
|
||||
@@ -304,7 +320,7 @@ public class NvHTTP {
|
||||
catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 401) {
|
||||
// Cert validation error - fall back to HTTP
|
||||
return openHttpConnectionToString(baseUrlHttp, "serverinfo", true);
|
||||
return openHttpConnectionToString(client, baseUrlHttp, "serverinfo");
|
||||
}
|
||||
|
||||
// If it's not a cert validation error, throw it
|
||||
@@ -315,13 +331,21 @@ public class NvHTTP {
|
||||
}
|
||||
else {
|
||||
// No pinned cert, so use HTTP
|
||||
return openHttpConnectionToString(baseUrlHttp , "serverinfo", true);
|
||||
return openHttpConnectionToString(client, baseUrlHttp, "serverinfo");
|
||||
}
|
||||
}
|
||||
|
||||
private static ComputerDetails.AddressTuple makeTuple(String address, int port) {
|
||||
if (address == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ComputerDetails.AddressTuple(address, port);
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerDetails() throws IOException, XmlPullParserException {
|
||||
public ComputerDetails getComputerDetails(boolean likelyOnline) throws IOException, XmlPullParserException {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
String serverInfo = getServerInfo();
|
||||
String serverInfo = getServerInfo(likelyOnline);
|
||||
|
||||
details.name = getXmlString(serverInfo, "hostname", false);
|
||||
if (details.name == null || details.name.isEmpty()) {
|
||||
@@ -331,11 +355,16 @@ public class NvHTTP {
|
||||
// UUID is mandatory to determine which machine is responding
|
||||
details.uuid = getXmlString(serverInfo, "uniqueid", true);
|
||||
|
||||
details.macAddress = getXmlString(serverInfo, "mac", false);
|
||||
details.localAddress = getXmlString(serverInfo, "LocalIP", false);
|
||||
details.httpsPort = getHttpsPort(serverInfo);
|
||||
|
||||
// This is missing on on recent GFE versions
|
||||
details.remoteAddress = getXmlString(serverInfo, "ExternalIP", false);
|
||||
details.macAddress = getXmlString(serverInfo, "mac", false);
|
||||
|
||||
// FIXME: Do we want to use the current port?
|
||||
details.localAddress = makeTuple(getXmlString(serverInfo, "LocalIP", false), baseUrlHttp.port());
|
||||
|
||||
// This is missing on on recent GFE versions, but it's present on Sunshine
|
||||
details.externalPort = getExternalPort(serverInfo);
|
||||
details.remoteAddress = makeTuple(getXmlString(serverInfo, "ExternalIP", false), details.externalPort);
|
||||
|
||||
details.pairState = getPairState(serverInfo);
|
||||
details.runningGameId = getCurrentGame(serverInfo);
|
||||
@@ -378,25 +407,18 @@ public class NvHTTP {
|
||||
.build();
|
||||
}
|
||||
|
||||
private ResponseBody openHttpConnection(HttpUrl baseUrl, String path, boolean enableReadTimeout) throws IOException {
|
||||
return openHttpConnection(baseUrl, path, null, enableReadTimeout);
|
||||
private ResponseBody openHttpConnection(OkHttpClient client, HttpUrl baseUrl, String path) throws IOException {
|
||||
return openHttpConnection(client, baseUrl, path, null);
|
||||
}
|
||||
|
||||
// Read timeout should be enabled for any HTTP query that requires no outside action
|
||||
// on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit.
|
||||
// The initial pair query does require outside action (user entering a PIN) but subsequent pairing
|
||||
// queries do not.
|
||||
private ResponseBody openHttpConnection(HttpUrl baseUrl, String path, String query, boolean enableReadTimeout) throws IOException {
|
||||
private ResponseBody openHttpConnection(OkHttpClient client, HttpUrl baseUrl, String path, String query) throws IOException {
|
||||
HttpUrl completeUrl = getCompleteUrl(baseUrl, path, query);
|
||||
Request request = new Request.Builder().url(completeUrl).get().build();
|
||||
Response response;
|
||||
|
||||
if (enableReadTimeout) {
|
||||
response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute();
|
||||
}
|
||||
else {
|
||||
response = performAndroidTlsHack(httpClient).newCall(request).execute();
|
||||
}
|
||||
Response response = performAndroidTlsHack(client).newCall(request).execute();
|
||||
|
||||
ResponseBody body = response.body();
|
||||
|
||||
@@ -417,13 +439,13 @@ public class NvHTTP {
|
||||
}
|
||||
}
|
||||
|
||||
private String openHttpConnectionToString(HttpUrl baseUrl, String path, boolean enableReadTimeout) throws IOException {
|
||||
return openHttpConnectionToString(baseUrl, path, null, enableReadTimeout);
|
||||
private String openHttpConnectionToString(OkHttpClient client, HttpUrl baseUrl, String path) throws IOException {
|
||||
return openHttpConnectionToString(client, baseUrl, path, null);
|
||||
}
|
||||
|
||||
private String openHttpConnectionToString(HttpUrl baseUrl, String path, String query, boolean enableReadTimeout) throws IOException {
|
||||
private String openHttpConnectionToString(OkHttpClient client, HttpUrl baseUrl, String path, String query) throws IOException {
|
||||
try {
|
||||
ResponseBody resp = openHttpConnection(baseUrl, path, query, enableReadTimeout);
|
||||
ResponseBody resp = openHttpConnection(client, baseUrl, path, query);
|
||||
String respString = resp.string();
|
||||
resp.close();
|
||||
|
||||
@@ -448,7 +470,7 @@ public class NvHTTP {
|
||||
}
|
||||
|
||||
public PairingManager.PairState getPairState() throws IOException, XmlPullParserException {
|
||||
return getPairState(getServerInfo());
|
||||
return getPairState(getServerInfo(true));
|
||||
}
|
||||
|
||||
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
|
||||
@@ -527,6 +549,32 @@ public class NvHTTP {
|
||||
}
|
||||
}
|
||||
|
||||
public int getHttpsPort(String serverInfo) {
|
||||
try {
|
||||
return Integer.parseInt(getXmlString(serverInfo, "HttpsPort", true));
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return DEFAULT_HTTPS_PORT;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return DEFAULT_HTTPS_PORT;
|
||||
}
|
||||
}
|
||||
|
||||
public int getExternalPort(String serverInfo) {
|
||||
// This is an extension which is not present in GFE. It is present for Sunshine to be able
|
||||
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
|
||||
try {
|
||||
return Integer.parseInt(getXmlString(serverInfo, "ExternalPort", true));
|
||||
} catch (XmlPullParserException e) {
|
||||
// Expected on non-Sunshine servers
|
||||
return baseUrlHttp.port();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return baseUrlHttp.port();
|
||||
}
|
||||
}
|
||||
|
||||
public NvApp getAppById(int appId) throws IOException, XmlPullParserException {
|
||||
LinkedList<NvApp> appList = getAppList();
|
||||
for (NvApp appFromList : appList) {
|
||||
@@ -618,7 +666,7 @@ public class NvHTTP {
|
||||
}
|
||||
|
||||
public String getAppListRaw() throws IOException {
|
||||
return openHttpConnectionToString(baseUrlHttps, "applist", true);
|
||||
return openHttpConnectionToString(httpClientLongConnectTimeout, getHttpsUrl(true), "applist");
|
||||
}
|
||||
|
||||
public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException {
|
||||
@@ -627,31 +675,28 @@ public class NvHTTP {
|
||||
return getAppListByReader(new StringReader(getAppListRaw()));
|
||||
}
|
||||
else {
|
||||
ResponseBody resp = openHttpConnection(baseUrlHttps, "applist", true);
|
||||
LinkedList<NvApp> appList = getAppListByReader(new InputStreamReader(resp.byteStream()));
|
||||
resp.close();
|
||||
return appList;
|
||||
try (final ResponseBody resp = openHttpConnection(httpClientLongConnectTimeout, getHttpsUrl(true), "applist")) {
|
||||
return getAppListByReader(new InputStreamReader(resp.byteStream()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String executePairingCommand(String additionalArguments, boolean enableReadTimeout) throws GfeHttpResponseException, IOException {
|
||||
return openHttpConnectionToString(baseUrlHttp, "pair",
|
||||
"devicename=roth&updateState=1&" + additionalArguments,
|
||||
enableReadTimeout);
|
||||
return openHttpConnectionToString(enableReadTimeout ? httpClientLongConnectTimeout : httpClientLongConnectNoReadTimeout,
|
||||
baseUrlHttp, "pair", "devicename=roth&updateState=1&" + additionalArguments);
|
||||
}
|
||||
|
||||
String executePairingChallenge() throws GfeHttpResponseException, IOException {
|
||||
return openHttpConnectionToString(baseUrlHttps, "pair",
|
||||
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||
true);
|
||||
return openHttpConnectionToString(httpClientLongConnectTimeout, getHttpsUrl(true),
|
||||
"pair", "devicename=roth&updateState=1&phrase=pairchallenge");
|
||||
}
|
||||
|
||||
public void unpair() throws IOException {
|
||||
openHttpConnectionToString(baseUrlHttp, "unpair", true);
|
||||
openHttpConnectionToString(httpClientLongConnectTimeout, baseUrlHttp, "unpair");
|
||||
}
|
||||
|
||||
public InputStream getBoxArt(NvApp app) throws IOException {
|
||||
ResponseBody resp = openHttpConnection(baseUrlHttps, "appasset", "appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true);
|
||||
ResponseBody resp = openHttpConnection(httpClientLongConnectTimeout, getHttpsUrl(true), "appasset", "appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0");
|
||||
return resp.byteStream();
|
||||
}
|
||||
|
||||
@@ -706,7 +751,7 @@ public class NvHTTP {
|
||||
enableSops = false;
|
||||
}
|
||||
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps, "launch",
|
||||
String xmlStr = openHttpConnectionToString(httpClientLongConnectNoReadTimeout, getHttpsUrl(true), "launch",
|
||||
"appid=" + appId +
|
||||
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + fps +
|
||||
"&additionalStates=1&sops=" + (enableSops ? 1 : 0) +
|
||||
@@ -716,8 +761,7 @@ public class NvHTTP {
|
||||
"&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) +
|
||||
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo() +
|
||||
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") +
|
||||
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""),
|
||||
false);
|
||||
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""));
|
||||
if (!getXmlString(xmlStr, "gamesession", true).equals("0")) {
|
||||
// sessionUrl0 will be missing for older GFE versions
|
||||
context.rtspSessionUrl = getXmlString(xmlStr, "sessionUrl0", false);
|
||||
@@ -729,11 +773,10 @@ public class NvHTTP {
|
||||
}
|
||||
|
||||
public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException {
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps, "resume",
|
||||
String xmlStr = openHttpConnectionToString(httpClientLongConnectNoReadTimeout, getHttpsUrl(true), "resume",
|
||||
"rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||
"&rikeyid="+context.riKeyId +
|
||||
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo(),
|
||||
false);
|
||||
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo());
|
||||
if (!getXmlString(xmlStr, "resume", true).equals("0")) {
|
||||
// sessionUrl0 will be missing for older GFE versions
|
||||
context.rtspSessionUrl = getXmlString(xmlStr, "sessionUrl0", false);
|
||||
@@ -745,14 +788,14 @@ public class NvHTTP {
|
||||
}
|
||||
|
||||
public boolean quitApp() throws IOException, XmlPullParserException {
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps, "cancel", false);
|
||||
String xmlStr = openHttpConnectionToString(httpClientLongConnectNoReadTimeout, getHttpsUrl(true), "cancel");
|
||||
if (getXmlString(xmlStr, "cancel", true).equals("0")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Newer GFE versions will just return success even if quitting fails
|
||||
// if we're not the original requestor.
|
||||
if (getCurrentGame(getServerInfo()) != 0) {
|
||||
if (getCurrentGame(getServerInfo(true)) != 0) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// that they can't kill someone else's stream.
|
||||
throw new GfeHttpResponseException(599, "");
|
||||
|
||||
@@ -30,6 +30,13 @@ public class MoonBridge {
|
||||
public static final int FRAME_TYPE_PFRAME = 0;
|
||||
public static final int FRAME_TYPE_IDR = 1;
|
||||
|
||||
public static final int COLORSPACE_REC_601 = 0;
|
||||
public static final int COLORSPACE_REC_709 = 1;
|
||||
public static final int COLORSPACE_REC_2020 = 2;
|
||||
|
||||
public static final int COLOR_RANGE_LIMITED = 0;
|
||||
public static final int COLOR_RANGE_FULL = 1;
|
||||
|
||||
public static final int CAPABILITY_DIRECT_SUBMIT = 1;
|
||||
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC = 2;
|
||||
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC = 4;
|
||||
@@ -45,6 +52,7 @@ public class MoonBridge {
|
||||
public static final int ML_ERROR_NO_VIDEO_FRAME = -101;
|
||||
public static final int ML_ERROR_UNEXPECTED_EARLY_TERMINATION = -102;
|
||||
public static final int ML_ERROR_PROTECTED_CONTENT = -103;
|
||||
public static final int ML_ERROR_FRAME_CONVERSION = -104;
|
||||
|
||||
public static final int ML_PORT_INDEX_TCP_47984 = 0;
|
||||
public static final int ML_PORT_INDEX_TCP_47989 = 1;
|
||||
@@ -271,7 +279,8 @@ public class MoonBridge {
|
||||
int clientRefreshRateX100,
|
||||
int encryptionFlags,
|
||||
byte[] riAesKey, byte[] riAesIv,
|
||||
int videoCapabilities);
|
||||
int videoCapabilities,
|
||||
int colorSpace, int colorRange);
|
||||
|
||||
public static native void stopConnection();
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import java.net.InetAddress;
|
||||
public class MdnsComputer {
|
||||
private InetAddress localAddr;
|
||||
private Inet6Address v6Addr;
|
||||
private int port;
|
||||
private String name;
|
||||
|
||||
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
|
||||
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr, int port) {
|
||||
this.name = name;
|
||||
this.localAddr = localAddress;
|
||||
this.v6Addr = v6Addr;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -26,6 +28,10 @@ public class MdnsComputer {
|
||||
return v6Addr;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
@@ -36,7 +42,7 @@ public class MdnsComputer {
|
||||
if (o instanceof MdnsComputer) {
|
||||
MdnsComputer other = (MdnsComputer)o;
|
||||
|
||||
if (!other.name.equals(name)) {
|
||||
if (!other.name.equals(name) || other.port != port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ public class MdnsDiscoveryAgent implements ServiceListener {
|
||||
// Add a computer object for each IPv4 address reported by the PC
|
||||
for (Inet4Address v4Addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr, info.getPort());
|
||||
if (computers.put(computer.getLocalAddress(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
@@ -273,7 +273,7 @@ public class MdnsDiscoveryAgent implements ServiceListener {
|
||||
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
||||
|
||||
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr, info.getPort());
|
||||
if (computers.put(v6LocalAddr != null ?
|
||||
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
|
||||
// This was a new entry
|
||||
|
||||
@@ -10,39 +10,75 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
public class WakeOnLanSender {
|
||||
private static final int[] PORTS_TO_TRY = new int[] {
|
||||
// These ports will always be tried as-is.
|
||||
private static final int[] STATIC_PORTS_TO_TRY = new int[] {
|
||||
9, // Standard WOL port (privileged port)
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||
};
|
||||
|
||||
// These ports will be offset by the base port number (47989) to support alternate ports.
|
||||
private static final int[] DYNAMIC_PORTS_TO_TRY = new int[] {
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
};
|
||||
|
||||
private static void sendPacketsForAddress(InetAddress address, int httpPort, DatagramSocket sock, byte[] payload) throws IOException {
|
||||
IOException lastException = null;
|
||||
boolean sentWolPacket = false;
|
||||
|
||||
// Try the static ports
|
||||
for (int port : STATIC_PORTS_TO_TRY) {
|
||||
try {
|
||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||
dp.setAddress(address);
|
||||
dp.setPort(port);
|
||||
sock.send(dp);
|
||||
sentWolPacket = true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
|
||||
// Try the dynamic ports
|
||||
for (int port : DYNAMIC_PORTS_TO_TRY) {
|
||||
try {
|
||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||
dp.setAddress(address);
|
||||
dp.setPort((port - 47989) + httpPort);
|
||||
sock.send(dp);
|
||||
sentWolPacket = true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sentWolPacket) {
|
||||
throw lastException;
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
||||
DatagramSocket sock = new DatagramSocket(0);
|
||||
byte[] payload = createWolPayload(computer);
|
||||
IOException lastException = null;
|
||||
boolean sentWolPacket = false;
|
||||
|
||||
try {
|
||||
// Try all resolved remote and local addresses and IPv4 broadcast address.
|
||||
try (final DatagramSocket sock = new DatagramSocket(0)) {
|
||||
// Try all resolved remote and local addresses and broadcast addresses.
|
||||
// The broadcast address is required to avoid stale ARP cache entries
|
||||
// making the sleeping machine unreachable.
|
||||
for (String unresolvedAddress : new String[] {
|
||||
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
|
||||
for (ComputerDetails.AddressTuple address : new ComputerDetails.AddressTuple[] {
|
||||
computer.localAddress, computer.remoteAddress,
|
||||
computer.manualAddress, computer.ipv6Address,
|
||||
}) {
|
||||
if (unresolvedAddress == null) {
|
||||
if (address == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
|
||||
// Try all the ports for each resolved address
|
||||
for (int port : PORTS_TO_TRY) {
|
||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||
dp.setAddress(resolvedAddress);
|
||||
dp.setPort(port);
|
||||
sock.send(dp);
|
||||
sentWolPacket = true;
|
||||
}
|
||||
sendPacketsForAddress(InetAddress.getByName("255.255.255.255"), address.port, sock, payload);
|
||||
for (InetAddress resolvedAddress : InetAddress.getAllByName(address.address)) {
|
||||
sendPacketsForAddress(resolvedAddress, address.port, sock, payload);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// We may have addresses that don't resolve on this subnet,
|
||||
@@ -52,8 +88,6 @@ public class WakeOnLanSender {
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sock.close();
|
||||
}
|
||||
|
||||
// Propagate the DNS resolution exception if we didn't
|
||||
@@ -65,18 +99,20 @@ public class WakeOnLanSender {
|
||||
|
||||
private static byte[] macStringToBytes(String macAddress) {
|
||||
byte[] macBytes = new byte[6];
|
||||
@SuppressWarnings("resource")
|
||||
Scanner scan = new Scanner(macAddress).useDelimiter(":");
|
||||
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
||||
try {
|
||||
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
|
||||
break;
|
||||
|
||||
try (@SuppressWarnings("resource")
|
||||
final Scanner scan = new Scanner(macAddress).useDelimiter(":")
|
||||
) {
|
||||
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
||||
try {
|
||||
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed MAC address: " + macAddress + " (index: " + i + ")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return macBytes;
|
||||
}
|
||||
scan.close();
|
||||
return macBytes;
|
||||
}
|
||||
|
||||
private static byte[] createWolPayload(ComputerDetails computer) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
@@ -96,7 +98,7 @@ public class AddComputerManually extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private void doAddPc(String host) throws InterruptedException {
|
||||
private void doAddPc(String rawUserInput) throws InterruptedException {
|
||||
boolean wrongSiteLocal = false;
|
||||
boolean success;
|
||||
int portTestResult;
|
||||
@@ -106,8 +108,28 @@ public class AddComputerManually extends Activity {
|
||||
|
||||
try {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
details.manualAddress = host;
|
||||
|
||||
// Use URI-style parsing for the host address input
|
||||
URI uri = new URI("moonlight://" + rawUserInput);
|
||||
|
||||
String host = uri.getHost();
|
||||
int port = uri.getPort();
|
||||
|
||||
// URI allows empty hosts, but we don't want that
|
||||
if (host == null || host.isEmpty()) {
|
||||
throw new URISyntaxException(rawUserInput, "Host failed to parse");
|
||||
}
|
||||
|
||||
// If a port was not specified, use the default
|
||||
if (port == -1) {
|
||||
port = NvHTTP.DEFAULT_HTTP_PORT;
|
||||
}
|
||||
|
||||
details.manualAddress = new ComputerDetails.AddressTuple(host, port);
|
||||
success = managerBinder.addComputerBlocking(details);
|
||||
if (!success){
|
||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Propagate the InterruptedException to the caller for proper handling
|
||||
dialog.dismiss();
|
||||
@@ -117,12 +139,12 @@ public class AddComputerManually extends Activity {
|
||||
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
||||
e.printStackTrace();
|
||||
success = false;
|
||||
} catch (URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
success = false;
|
||||
}
|
||||
|
||||
// Keep the SpinnerDialog open while testing connectivity
|
||||
if (!success){
|
||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||
}
|
||||
if (!success && !wrongSiteLocal) {
|
||||
// Run the test before dismissing the spinner because it can take a few seconds.
|
||||
portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER, 443,
|
||||
|
||||
@@ -27,7 +27,7 @@ import java.security.cert.CertificateEncodingException;
|
||||
public class ServerHelper {
|
||||
public static final String CONNECTION_TEST_SERVER = "android.conntest.moonlight-stream.org";
|
||||
|
||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) throws IOException {
|
||||
public static ComputerDetails.AddressTuple getCurrentAddressFromComputer(ComputerDetails computer) throws IOException {
|
||||
if (computer.activeAddress == null) {
|
||||
throw new IOException("No active address for "+computer.name);
|
||||
}
|
||||
@@ -56,7 +56,9 @@ public class ServerHelper {
|
||||
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
Intent intent = new Intent(parent, Game.class);
|
||||
intent.putExtra(Game.EXTRA_HOST, computer.activeAddress);
|
||||
intent.putExtra(Game.EXTRA_HOST, computer.activeAddress.address);
|
||||
intent.putExtra(Game.EXTRA_PORT, computer.activeAddress.port);
|
||||
intent.putExtra(Game.EXTRA_HTTPS_PORT, computer.httpsPort);
|
||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||
@@ -126,7 +128,7 @@ public class ServerHelper {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), computer.httpsPort,
|
||||
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
|
||||
if (httpConn.quitApp()) {
|
||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||
|
||||
@@ -386,7 +386,8 @@ Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass c
|
||||
jint clientRefreshRateX100,
|
||||
jint encryptionFlags,
|
||||
jbyteArray riAesKey, jbyteArray riAesIv,
|
||||
jint videoCapabilities) {
|
||||
jint videoCapabilities,
|
||||
jint colorSpace, jint colorRange) {
|
||||
SERVER_INFORMATION serverInfo = {
|
||||
.address = (*env)->GetStringUTFChars(env, address, 0),
|
||||
.serverInfoAppVersion = (*env)->GetStringUTFChars(env, appVersion, 0),
|
||||
@@ -406,6 +407,8 @@ Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass c
|
||||
.hevcBitratePercentageMultiplier = hevcBitratePercentageMultiplier,
|
||||
.clientRefreshRateX100 = clientRefreshRateX100,
|
||||
.encryptionFlags = encryptionFlags,
|
||||
.colorSpace = colorSpace,
|
||||
.colorRange = colorRange
|
||||
};
|
||||
|
||||
jbyte* riAesKeyBuf = (*env)->GetByteArrayElements(env, riAesKey, NULL);
|
||||
|
||||
Submodule app/src/main/jni/moonlight-core/moonlight-common-c updated: 50603ac16e...50c0a51b10
@@ -4,11 +4,23 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".Game" >
|
||||
|
||||
<View
|
||||
android:id="@+id/backgroundTouchView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<com.limelight.ui.StreamView
|
||||
android:id="@+id/surfaceView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center" />
|
||||
android:layout_gravity="center"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:focusedByDefault="true"
|
||||
android:defaultFocusHighlightEnabled="false">
|
||||
<requestFocus />
|
||||
</com.limelight.ui.StreamView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/performanceOverlay"
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="pair_incorrect_pin">Неправилен ПИН</string>
|
||||
<string name="no_video_received_error">Няма получено видео от хоста.</string>
|
||||
<string name="pacing_latency">Предпочитане на най-ниската латентност</string>
|
||||
<string name="pacing_balanced_alt">Балансирано с FPS лимит</string>
|
||||
<string name="pacing_smoothness">Предпочитане на най-плавно видео (може значително да увеличи латентността)</string>
|
||||
<string name="conn_metered">Предупреждение: Вашата активна мрежова връзка се измерва!</string>
|
||||
<string name="conn_client_latency_hw">латентност на хардуерния декодер:</string>
|
||||
<string name="conn_hardware_latency">Средна латентност на хардуерно декодиране:</string>
|
||||
<string name="ip_hint">IP адрес на GeForce PC</string>
|
||||
<string name="pcview_menu_send_wol">Изпращане на Wake-On-LAN заявка</string>
|
||||
<string name="pcview_menu_delete_pc">Изтрий компютъра</string>
|
||||
<string name="pcview_menu_test_network">Тестване на мрежовата връзка</string>
|
||||
<string name="pcview_menu_details">Детайли</string>
|
||||
<string name="nettest_title_waiting">Тестване на мрежовата връзка</string>
|
||||
<string name="nettest_title_done">Тестът на мрежата е завършен</string>
|
||||
<string name="pairing">Сдвояване…</string>
|
||||
<string name="pair_pc_offline">Компютърът е офлайн</string>
|
||||
<string name="pair_pc_ingame">Компютърът в момента е в игра. Трябва да затворите играта преди сдвояване.</string>
|
||||
<string name="pair_pairing_title">Сдвояване</string>
|
||||
<string name="wol_waking_pc">Събуждащ се компютъра…</string>
|
||||
<string name="unpair_fail">Неуспешно раздвояване</string>
|
||||
<string name="unpair_error">Устройството не беше сдвоено</string>
|
||||
<string name="error_pc_offline">Компютърът е офлайн</string>
|
||||
<string name="title_decoding_error">Видеодекодерът крашна</string>
|
||||
<string name="no_frame_received_error">Мрежовата ви връзка не работи добре. Намалете настройката си за битрейт на видео или опитайте с по-бърза връзка.</string>
|
||||
<string name="conn_client_latency">Средна латентност при декодиране на кадър:</string>
|
||||
<string name="conn_starting">Стартиране</string>
|
||||
<string name="conn_terminated_title">Връзката е прекратена</string>
|
||||
<string name="yes">Да</string>
|
||||
<string name="applist_menu_hide_app">Скриване на приложението</string>
|
||||
<string name="applist_refresh_title">Списък с приложения</string>
|
||||
<string name="summary_resolution_list">Увеличете, за да подобрите яснотата на изображението. Намалете за по-добра производителност на устройства от по-нисък клас и по-бавни мрежи.</string>
|
||||
<string name="title_fps_list">Кадрова честота на видео</string>
|
||||
<string name="scut_deleted_pc">Компютърът е изтрит</string>
|
||||
<string name="scut_not_paired">Компютърът не е сдвоен</string>
|
||||
<string name="scut_pc_not_found">Компютърът не е намерен</string>
|
||||
<string name="scut_invalid_uuid">Предоставеният компютър не е валиден</string>
|
||||
<string name="scut_invalid_app_id">Предоставеното приложение не е валидно</string>
|
||||
<string name="help_loading_msg">Помощната страница се зарежда…</string>
|
||||
<string name="help_loading_title">Помощ</string>
|
||||
<string name="pcview_menu_header_online">Онлайн</string>
|
||||
<string name="pcview_menu_header_unknown">Презареждане</string>
|
||||
<string name="pcview_menu_pair_pc">Сдвояване с компютър</string>
|
||||
<string name="pcview_menu_header_offline">Извън линия</string>
|
||||
<string name="pcview_menu_app_list">Вижте Всички Приложения</string>
|
||||
<string name="pcview_menu_unpair_pc">Раздвояване</string>
|
||||
<string name="nettest_text_waiting">Moonlight тества вашата мрежова връзка, за да определи дали NVIDIA GameStream е блокиран.
|
||||
\n
|
||||
\nТова може да отнеме няколко секунди…</string>
|
||||
<string name="nettest_text_success">Вашата мрежа изглежда не блокира Moonlight. Ако все още имате проблеми със свързването, проверете настройките на защитната стена на вашия компютър.
|
||||
\n
|
||||
\nАко се опитвате да стриймвате през интернет, инсталирайте Moonlight Internet Hosting Tool на вашия компютър и стартирайте включения тестер за интернет стрийминг, за да проверите интернет връзката на вашия компютър.</string>
|
||||
<string name="nettest_text_inconclusive">Мрежовият тест не можа да бъде извършен, защото нито един от сървърите за тестване на връзката на Moonlight не е достъпен. Проверете връзката си с интернет или опитайте отново по-късно.</string>
|
||||
<string name="nettest_text_failure">Текущата мрежова връзка на вашето устройство изглежда блокира Moonlight. Стриймването през интернет може да не работи, докато сте свързани към тази мрежа.
|
||||
\n
|
||||
\nСледните мрежови портове са блокирани:
|
||||
\n</string>
|
||||
<string name="nettest_text_blocked">Текущата мрежова връзка на вашето устройство блокира Moonlight. Стриймването през интернет може да не работи, докато сте свързани към тази мрежа.</string>
|
||||
<string name="pair_pairing_msg">Моля, въведете следния ПИН на избрания компютър:</string>
|
||||
<string name="pair_fail">Неуспешно сдвояване</string>
|
||||
<string name="pair_already_in_progress">Сдвояването вече е в ход</string>
|
||||
<string name="wol_pc_online">Компютърът е онлайн</string>
|
||||
<string name="wol_no_mac">Компютърът не може да бъде събуден, защото GFE не изпрати MAC адрес</string>
|
||||
<string name="wol_fail">Неуспешно изпращане на Wake-On-LAN пакети</string>
|
||||
<string name="wol_waking_msg">Може да отнеме няколко секунди, докато вашият компютър се събуди. Ако не стане, уверете се, че е конфигуриран правилно за Wake-On-LAN.</string>
|
||||
<string name="unpairing">Раздвояване…</string>
|
||||
<string name="unpair_success">Раздвояването бе успешно</string>
|
||||
<string name="video_decoder_init_failed">Видео декодерът не успя да се инициализира. Вашето устройство може да не поддържа избраната резолюция или честота.</string>
|
||||
<string name="error_manager_not_running">Услугата ComputerManager не работи. Моля, изчакайте няколко секунди или рестартирайте приложението.</string>
|
||||
<string name="error_404">GFE върна грешка HTTP 404. Уверете се, че вашият компютър има поддръжана видео карта. Използването на софтуер за отдалечен работен плот също може да причини тази грешка. Опитайте да рестартирате машината си или да преинсталирате GFE.</string>
|
||||
<string name="message_decoding_error">Moonlight претърпя срив поради несъвместимост с видеодекодера на това устройство. Уверете се, че GeForce Experience е актуализиран до най-новата версия на вашия компютър. Опитайте да коригирате настройките за стриймване, ако сривовете продължат.</string>
|
||||
<string name="title_decoding_reset">Нулиране на видео настройките</string>
|
||||
<string name="message_decoding_reset">Видео декодерът на вашето устройство продължава да се срива при избраните от вас настройки за стриймване. Настройките са нулирани по подразбиране.</string>
|
||||
<string name="audioconf_stereo">Стерео</string>
|
||||
<string name="applist_refresh_msg">Приложенията се опресняват…</string>
|
||||
<string name="error_usb_prohibited">USB достъпът е забранен от администратора на вашето устройство. Проверете настройките на Knox или MDM.</string>
|
||||
<string name="audioconf_71surround">7.1 съраунд звук</string>
|
||||
<string name="audioconf_51surround">5.1 съраунд звук</string>
|
||||
<string name="videoformat_hevcauto">Автоматично</string>
|
||||
<string name="videoformat_hevcalways">Винаги използване на HEVC (може да крашне)</string>
|
||||
<string name="videoformat_hevcnever">Никога да не се използва HEVC</string>
|
||||
<string name="summary_frame_pacing">Посочете как да се балансира забавянето и плавността на видеото</string>
|
||||
<string name="title_frame_pacing">Стъпка на видео кадрите</string>
|
||||
<string name="pacing_balanced">Балансирано</string>
|
||||
<string name="check_ports_msg">Проверете вашата защитна стена и правилата за препращане на портове за порт(ове):</string>
|
||||
<string name="conn_establishing_title">Установяване на връзка</string>
|
||||
<string name="conn_establishing_msg">Стартиране на връзка</string>
|
||||
<string name="conn_error_msg">Неуспешно стартиране</string>
|
||||
<string name="conn_terminated_msg">Връзката беше прекратена</string>
|
||||
<string name="conn_error_title">Грешка при свързване</string>
|
||||
<string name="no">Не</string>
|
||||
<string name="help">Помощ</string>
|
||||
<string name="lost_connection">Загубена връзка с компютър</string>
|
||||
<string name="title_details">Подробности</string>
|
||||
<string name="delete_pc_msg">Сигурни ли сте, че искате да изтриете този компютър\?</string>
|
||||
<string name="poor_connection_msg">Лоша връзка с компютъра</string>
|
||||
<string name="perf_overlay_streamdetails">Видео поток: %1$s %2$.2f FPS</string>
|
||||
<string name="perf_overlay_decoder">Декодер: %1$s</string>
|
||||
<string name="perf_overlay_netdrops">Кадри, пропуснати от вашата мрежова връзка: %1$.2f%%</string>
|
||||
<string name="perf_overlay_incomingfps">Входяща честота на кадрите от мрежата: %1$.2f FPS</string>
|
||||
<string name="perf_overlay_netlatency">Средно забавяне на мрежата: %1$d ms (variance: %2$d ms)</string>
|
||||
<string name="applist_connect_msg">Свързване с компютъра…</string>
|
||||
<string name="perf_overlay_renderingfps">Кадрова честота на изобразяване: %1$.2f FPS</string>
|
||||
<string name="perf_overlay_dectime">Средно време за декодиране: %1$.2f ms</string>
|
||||
<string name="applist_menu_quit">Прекратяване на сесията</string>
|
||||
<string name="msg_add_pc">Свързване към компютъра…</string>
|
||||
<string name="applist_menu_resume">Възобновяване на сесията</string>
|
||||
<string name="applist_menu_quit_and_start">Изключване на текущата игра и стартиране</string>
|
||||
<string name="applist_menu_cancel">Отказ</string>
|
||||
<string name="applist_menu_details">Виж детайлите</string>
|
||||
<string name="applist_menu_scut">Създаване на пряк път</string>
|
||||
<string name="applist_refresh_error_title">Грешка</string>
|
||||
<string name="applist_menu_tv_channel">Добавяне към канал</string>
|
||||
<string name="applist_refresh_error_msg">Неуспешно получаване на списък с приложения</string>
|
||||
<string name="applist_quit_success">Успешно изключване</string>
|
||||
<string name="applist_quit_app">Изключване</string>
|
||||
<string name="applist_quit_fail">Неуспешно изключване</string>
|
||||
<string name="applist_details_id">ID на приложението:</string>
|
||||
<string name="applist_quit_confirmation">Сигурни ли сте, че искате да затворите работещото приложение\? Всички незапазени данни ще бъдат загубени.</string>
|
||||
<string name="title_add_pc">Ръчно добавяне на компютър</string>
|
||||
<string name="addpc_fail">Неуспешна връзка с посочения компютър. Уверете се, че необходимите портове са разрешени през защитната стена.</string>
|
||||
<string name="category_basic_settings">Основни настройки</string>
|
||||
<string name="addpc_success">Успешно добавен компютър</string>
|
||||
<string name="addpc_enter_ip">Трябва да въведете IP адрес</string>
|
||||
<string name="addpc_wrong_sitelocal">Този адрес не изглежда правилен. Трябва да използвате публичния IP адрес на вашия рутер за стриймване през интернет.</string>
|
||||
<string name="title_resolution_list">Видео резолюция</string>
|
||||
<string name="summary_fps_list">Увеличете за по-плавен видео поток. Намалете за по-добра производителност на устройства от по-нисък клас.</string>
|
||||
<string name="title_seekbar_bitrate">Видео битрейт</string>
|
||||
<string name="suffix_seekbar_bitrate_mbps">Mbps</string>
|
||||
<string name="title_audio_config_list">Конфигурация на съраунд звук</string>
|
||||
<string name="summary_audio_config_list">Активиране на 5.1 или 7.1 съраунд звук за системи за домашно кино</string>
|
||||
<string name="summary_seekbar_bitrate">Увеличете за по-добро качество на изображението. Намалете, за да подобрите производителността при по-бавни връзки.</string>
|
||||
<string name="resolution_prefix_native_portrait">(Портрет)</string>
|
||||
<string name="title_checkbox_enable_audiofx">Активиране на поддръжката на системния еквалайзер</string>
|
||||
<string name="title_checkbox_stretch_video">Разтегляне на видеото на цял екран</string>
|
||||
<string name="resolution_prefix_native_landscape">(Пейзаж)</string>
|
||||
<string name="category_audio_settings">Аудио настройки</string>
|
||||
<string name="category_input_settings">Настройки за въвеждане</string>
|
||||
<string name="title_checkbox_touchscreen_trackpad">Използване на сензорния екран като тракпад</string>
|
||||
<string name="title_checkbox_multi_controller">Автоматично откриване на наличен контролер</string>
|
||||
<string name="summary_checkbox_multi_controller">Премахването на отметката от тази опция принуждава контролер винаги да присъства</string>
|
||||
</resources>
|
||||
@@ -259,4 +259,7 @@
|
||||
<string name="title_checkbox_reduce_refresh_rate">Aktualisierungsrate verringern erlauben</string>
|
||||
<string name="summary_checkbox_reduce_refresh_rate">Durch das Verringern der Display Aktualisierungsrate, kann, auf Kosten der Video-Latenz, der Akkuverbrauch reduziert werden</string>
|
||||
<string name="resolution_prefix_native_landscape">(Landscape)</string>
|
||||
<string name="frame_conversion_error">Der Host-PC hat einen schwerwiegenden Videocodierungsfehler gemeldet.
|
||||
\n
|
||||
\nVersuchen Sie, den HDR-Modus zu deaktivieren, die Streaming-Auflösung zu ändern oder die Bildschirmauflösung des Host-PCs zu ändern.</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -191,7 +191,7 @@
|
||||
<string name="title_enable_perf_overlay">스트리밍 중 성능 정보 표시</string>
|
||||
<string name="summary_enable_perf_overlay">스트리밍하는 동안 실시간 스트림 성능 정보 표시</string>
|
||||
<string name="summary_enable_hdr">게임 및 PC의 GPU가 HDR을 지원하는 경우 HDR을 활성화합니다. HDR에는 GTX 1000 시리즈 또는 그 이상의 GPU가 필요합니다.</string>
|
||||
<string name="title_enable_hdr">HDR활성화 (실험중인 기능)</string>
|
||||
<string name="title_enable_hdr">HDR활성화 (실험용)</string>
|
||||
<string name="summary_disable_frame_drop">일부 장치에서 미세한 끊김 현상을 줄일 수 있지만 지연 시간이 늘어날 수 있습니다.</string>
|
||||
<string name="title_disable_frame_drop">프레임드랍 최적화</string>
|
||||
<string name="summary_unlock_fps">90 또는 120FPS로 스트리밍하면 지연 시간이 줄어들 수 있지만 이를 지원할 수 없는 장치에서는 지연 또는 불안정 할 수 있습니다</string>
|
||||
@@ -255,4 +255,11 @@
|
||||
<string name="title_frame_pacing">비디오 프레임 처리방식</string>
|
||||
<string name="pacing_smoothness">가장 부드러운 비디오 선호(대기 시간이 크게 증가할 수 있음)</string>
|
||||
<string name="category_help">도움말</string>
|
||||
<string name="resolution_prefix_native_landscape">(가로)</string>
|
||||
<string name="resolution_prefix_native_portrait">(세로)</string>
|
||||
<string name="title_checkbox_reduce_refresh_rate">주사율 감소 허용</string>
|
||||
<string name="summary_checkbox_reduce_refresh_rate">화면 주사율을 낮춰서 영상 지연 시간이 증가하고 전력을 절약할 수 있습니다.</string>
|
||||
<string name="frame_conversion_error">호스트 PC에서 치명적인 비디오 인코딩 오류를 보고했습니다.
|
||||
\n
|
||||
\nHDR 모드를 비활성화하거나 스트리밍 해상도를 변경하거나 호스트 PC의 디스플레이 해상도를 변경해 보십시오.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="scut_deleted_pc">PC usunięty</string>
|
||||
<string name="scut_pc_not_found">PC nie znaleziony</string>
|
||||
<string name="pcview_menu_header_unknown">Odświeżanie</string>
|
||||
<string name="scut_not_paired">PC niesparowany</string>
|
||||
</resources>
|
||||
@@ -84,7 +84,7 @@
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Общие Настройки</string>
|
||||
<string name="title_resolution_list">Выберите разрешение и частоту кадров</string>
|
||||
<string name="summary_resolution_list">Выбор слишком высокого значеня для своего устройства может вызвать тормоза или вылеты</string>
|
||||
<string name="summary_resolution_list">Увеличьте для повышения чёткости изображения. Уменьшите для лучшей производительности на медленных устройствах или сетях.</string>
|
||||
<string name="title_seekbar_bitrate">Выберите битрейт видео</string>
|
||||
<string name="summary_seekbar_bitrate">Низкий битрейт уменьшит зависания. Увеличение битрейта улучшит качество изображения.</string>
|
||||
<string name="title_checkbox_stretch_video">Растягивать видео на весь экран</string>
|
||||
@@ -184,7 +184,7 @@
|
||||
<string name="scut_invalid_uuid">Указанный PC недействителен</string>
|
||||
<string name="scut_invalid_app_id">Указанное приложение недействительно</string>
|
||||
<string name="suffix_osc_opacity">%</string>
|
||||
<string name="suffix_seekbar_bitrate_mbps">Мб/с</string>
|
||||
<string name="suffix_seekbar_bitrate_mbps">Мбит/с</string>
|
||||
<string name="applist_menu_hide_app">Скрыть приложение</string>
|
||||
<string name="pcview_menu_test_network">Тестовое подключение к сети</string>
|
||||
<string name="pcview_menu_header_unknown">Обновление</string>
|
||||
@@ -213,13 +213,46 @@
|
||||
<string name="perf_overlay_streamdetails">Видеострим: %1$s %2$.2f FPS</string>
|
||||
<string name="resolution_prefix_native_fullscreen">Родной полноэкранный режим</string>
|
||||
<string name="perf_overlay_netlatency">Средняя задержка сети: %1$d мс (дисперсия: %2$d мс)</string>
|
||||
|
||||
<!-- Array strings -->
|
||||
<string name="audioconf_stereo">Стерео</string>
|
||||
<string name="audioconf_51surround">5.1 Объёмный звук</string>
|
||||
<string name="audioconf_71surround">7.1 Объёмный звук</string>
|
||||
|
||||
<string name="videoformat_hevcauto">Использовать HEVC только если безопасно</string>
|
||||
<string name="videoformat_hevcauto">Автоматически</string>
|
||||
<string name="videoformat_hevcalways">Всегда использовать HEVC если доступно</string>
|
||||
<string name="videoformat_hevcnever">Никогда не использовать HEVC</string>
|
||||
<string name="pacing_latency">Минимальная задержка</string>
|
||||
<string name="pacing_balanced">Баланс</string>
|
||||
<string name="pacing_smoothness">Максимальная плавность (может значительно увеличить задержку)</string>
|
||||
<string name="title_checkbox_enable_audiofx">Включить поддержку системного эквалайзера</string>
|
||||
<string name="summary_checkbox_absolute_mouse_mode">Может сделать движения мыши естественней для удаленного рабочего стола, но несовместимо с большинством игр.</string>
|
||||
<string name="summary_checkbox_reduce_refresh_rate">Сниженная частота обновления может экономить батарею за счет дополнительной задержки видео</string>
|
||||
<string name="resolution_prefix_native_landscape">(Горизонтальный)</string>
|
||||
<string name="fps_60">60 FPS</string>
|
||||
<string name="fps_90">90 FPS</string>
|
||||
<string name="fps_120">120 FPS</string>
|
||||
<string name="frame_conversion_error">Хост-ПК сообщил о критической ошибке кодирования видео.
|
||||
\n
|
||||
\nПопробуйте выключить HDR, изменить разрешение стрима или разрешение экрана на хост-ПК.</string>
|
||||
<string name="summary_checkbox_enable_audiofx">Позволяет работать аудио эффектам во время стрима, но может увеличить задержку звука</string>
|
||||
<string name="title_setup_guide">Инструкция по настройке</string>
|
||||
<string name="summary_setup_guide">Посмотреть инструкцию о том как настроить ваш пк для стриминга</string>
|
||||
<string name="title_troubleshooting">Руководство по устранению неполадок</string>
|
||||
<string name="summary_frame_pacing">Укажите как сбалансировать задержку и плавностью видео</string>
|
||||
<string name="summary_privacy_policy">Посмотреть политику конфиденциальности Moonlight</string>
|
||||
<string name="title_privacy_policy">Политика конфиденциальности</string>
|
||||
<string name="resolution_360p">360p</string>
|
||||
<string name="resolution_1080p">1080p</string>
|
||||
<string name="resolution_1440p">1440p</string>
|
||||
<string name="summary_troubleshooting">Посмотрите советы по обнаружению и устранению проблем</string>
|
||||
<string name="category_help">Помощь</string>
|
||||
<string name="resolution_720p">720p</string>
|
||||
<string name="resolution_prefix_native_portrait">(Портретный)</string>
|
||||
<string name="summary_seekbar_deadzone">Некоторые игры могут принудительно увеличить мёртвую зону вместо указанной в Moonlight.</string>
|
||||
<string name="title_checkbox_absolute_mouse_mode">Режим удалённого рабочего стола для мыши</string>
|
||||
<string name="title_checkbox_reduce_refresh_rate">Разрешить снижение частоты обновления</string>
|
||||
<string name="resolution_4k">4K</string>
|
||||
<string name="fps_30">30 FPS</string>
|
||||
<string name="title_frame_pacing">Скорость вывода/отрисовки кадра</string>
|
||||
<string name="pacing_balanced_alt">Сбалансированно с лимитом FPS</string>
|
||||
<string name="resolution_480p">480p</string>
|
||||
</resources>
|
||||
@@ -2,5 +2,7 @@
|
||||
<style name="AppBaseTheme" parent="android:Theme.Material">
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<!-- Avoid some systems like MIUI which break the visibility of games title -->
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -228,4 +228,11 @@
|
||||
<string name="videoformat_hevcauto">Chỉ sử dụng HEVC nếu ổn định</string>
|
||||
<string name="videoformat_hevcalways">Luôn sử dụng HEVC (có thể gây crash)</string>
|
||||
<string name="videoformat_hevcnever">Không bao giờ sử dụng HEVC</string>
|
||||
<string name="resolution_prefix_native_landscape">(Ngang)</string>
|
||||
<string name="frame_conversion_error">PC chủ đã gặp lỗi mã hoá video nghiêm trọng.
|
||||
\n
|
||||
\nHãy thử tắt chế độ HDR, đổi độ phân giải stream, hoặc đổi độ phân giải màn hình PC chủ.</string>
|
||||
<string name="title_checkbox_enable_audiofx">Hỗ trợ bộ cân bằng của hệ thống</string>
|
||||
<string name="summary_checkbox_enable_audiofx">Cho phép hiệu ứng âm thanh hoạt động khi stream, nhưng có thể tăng độ trễ âm thanh</string>
|
||||
<string name="resolution_prefix_native_portrait">(Dọc)</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,13 @@
|
||||
<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>
|
||||
<string name="frame_conversion_error">主机 PC 报告了致命的视频编码错误。
|
||||
\n
|
||||
\n尝试禁用 HDR 模式、更改流分辨率或更改主机 PC 的显示分辨率。</string>
|
||||
</resources>
|
||||
@@ -40,7 +40,7 @@
|
||||
<string name="error_pc_offline">電腦已離線</string>
|
||||
<string name="error_manager_not_running">ComputerManager 服務未執行。請稍等幾秒或重啟應用程式。</string>
|
||||
<string name="error_unknown_host"> 無法解析主機位址 </string>
|
||||
<string name="error_404">GFE 返回了 HTTP 404 錯誤。確保你的電腦顯示卡支援串流。使用遠端桌面軟體同樣會引起此錯誤,請嘗試重啟電腦或重新安裝 GFE。</string>
|
||||
<string name="error_404">GFE 傳回了 HTTP 404 錯誤。確保你的電腦顯示卡支援串流。使用遠端桌面軟體同樣會引起此錯誤,請嘗試重啟電腦或重新安裝 GFE。</string>
|
||||
<string name="title_decoding_error">視訊解碼器崩潰</string>
|
||||
<string name="message_decoding_error">由於與該裝置的視訊解碼器不相容,Moonlight 已崩潰。確保你電腦上的 GFE 已更新至最新版本,如果崩潰繼續,請嘗試調整串流設定。</string>
|
||||
<string name="title_decoding_reset">重設視訊設定</string>
|
||||
@@ -254,4 +254,11 @@
|
||||
<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>
|
||||
<string name="frame_conversion_error">主機電腦回報了一個嚴重的視訊編碼錯誤。
|
||||
\n
|
||||
\n嘗試停用 HDR 模式,變更串流解析度,或變更您的主機電腦的顯示器解析度。</string>
|
||||
</resources>
|
||||
@@ -74,6 +74,7 @@
|
||||
<string name="no_video_received_error">No video received from host.</string>
|
||||
<string name="no_frame_received_error">Your network connection isn\'t performing well. Reduce your video bitrate setting or try a faster connection.</string>
|
||||
<string name="early_termination_error">Something went wrong on your host PC when starting the stream.\n\nMake sure you don\'t have any DRM-protected content open on your host PC. You can also try restarting your host PC.\n\nIf the issue persists, try reinstalling your GPU drivers and GeForce Experience.</string>
|
||||
<string name="frame_conversion_error">The host PC reported a fatal video encoding error.\n\nTry disabling HDR mode, changing the streaming resolution, or changing your host PC\'s display resolution.</string>
|
||||
<string name="check_ports_msg">Check your firewall and port forwarding rules for port(s):</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
- Fixed several input bugs with the on-screen gamepad
|
||||
- Implemented recovery logic for video decoder errors
|
||||
- Updated community contributed translations from Weblate
|
||||
@@ -0,0 +1 @@
|
||||
- Added support for GeForce Experience 3.26
|
||||
@@ -0,0 +1,6 @@
|
||||
- Qualcomm devices now use HEVC by default for improved efficiency
|
||||
- Added system key capture on Samsung devices running Android 10 or later
|
||||
- Improved frame loss handling when using HEVC
|
||||
- Fixed streaming crash on devices running Android 4.1 to 4.4
|
||||
- Fixed streaming at resolutions below 720x540 with GeForce Experience 3.26
|
||||
- Updated community contributed translations from Weblate
|
||||
@@ -0,0 +1,6 @@
|
||||
- Added support for custom ports when hosting on Sunshine
|
||||
- Enabled HEVC by default on additional devices
|
||||
- Enabled fast HEVC frame loss recovery on additional devices
|
||||
- Improved audio quality when streaming remotely
|
||||
- Improved video performance and audio quality when streaming locally over IPv6
|
||||
- Updated community contributed translations from Weblate
|
||||
Reference in New Issue
Block a user