Compare commits

..
44 Commits
Author SHA1 Message Date
Cameron Gutman 1ac721a35b Bump to version 5.1.2 2017-08-13 18:51:22 -07:00
Cameron Gutman e49b1c92a2 Update for AS 3.0 Beta 2 2017-08-13 18:37:31 -07:00
Cameron Gutman db4295bf83 Add adaptive icon for PC shortcut 2017-08-13 18:31:09 -07:00
Cameron Gutman 824c37f9d5 Adaptive launcher icon 2017-08-13 18:06:53 -07:00
Cameron Gutman acf4426952 Update for Gradle 4 2017-06-24 12:56:52 -07:00
Cameron Gutman e8c50342ab Version 5.1.1 2017-06-17 16:06:57 -07:00
Cameron Gutman 598995de3b Fix audio renderer using non-existant classes on Lollipop 2017-06-16 20:27:03 -07:00
Cameron Gutman 01cf0cc649 Fix Lint error 2017-06-16 20:06:45 -07:00
Cameron Gutman fa560f462f Add battery saver mode 2017-06-16 20:01:41 -07:00
Cameron Gutman f6e40118a9 Bring back the warning displayed if video decoder initialization fails 2017-06-16 19:50:50 -07:00
Cameron Gutman fe7148dbd4 Only throw decoder exceptions if we're still receiving them after 3 seconds 2017-06-16 19:39:15 -07:00
Cameron Gutman 60de065836 Cleanup video decoder teardown paths 2017-06-16 19:11:39 -07:00
Cameron Gutman 6f82f82abb Use low latency audio pathway on Lollipop and later 2017-06-16 19:08:15 -07:00
Cameron Gutman 42f18cb4ac Version 5.1 2017-06-11 17:14:15 -07:00
Cameron Gutman 1bbd0054c2 Update common again to fix another long haul testing bug 2017-06-11 14:56:41 -07:00
Cameron Gutman acdde37a3a Update common library again 2017-06-11 13:54:05 -07:00
Cameron Gutman ad40e12167 Update common to fix incorrect assert firing 2017-06-11 13:09:29 -07:00
Cameron Gutman 1b3322b5ee Suppress crashes if the surface has become invalid 2017-06-10 17:25:23 -07:00
Cameron Gutman 6340ec6c6d Consolidate handling of decoder exceptions 2017-06-10 16:57:37 -07:00
Cameron Gutman babd92c8c0 Add additional information to total frame latency and RendererException 2017-06-10 16:45:07 -07:00
Cameron Gutman 7f1fe5f520 Fix NPE if the device doesn't support H.264 hardware decoding 2017-06-10 11:48:25 -07:00
Cameron Gutman 01458770d2 Fix NPE enumerating input devices 2017-06-10 11:45:12 -07:00
Cameron Gutman 8d05f044f5 Allow software decoding on the emulator for testing 2017-06-08 22:21:51 -07:00
Cameron Gutman f5680b59a5 Use debug moonlight-common with asserts enabled on debug builds and release moonlight-common with asserts disabled on release builds 2017-06-08 19:57:55 -07:00
Cameron Gutman 0ecf86c7ed At long last, Android has native mouse capture. Don't show the root version to users running O 2017-06-08 18:26:12 -07:00
Cameron Gutman 6789e8d497 Immediately call stopConnection() rather than waiting for activity stop on connection failure 2017-06-08 18:24:22 -07:00
Cameron Gutman 7d0160d556 Update gradle and SDK to O 2017-06-08 18:17:59 -07:00
Cameron Gutman f6a0990432 Final fixes for Android O pointer capture 2017-06-08 18:17:34 -07:00
Cameron Gutman 5d6094df97 Version 5.0.2 2017-06-08 17:57:57 -07:00
Cameron Gutman d98d4aeda2 Fix FEC fencepost error in moonlight-common 2017-06-08 17:57:43 -07:00
Cameron Gutman 852dcf5a2d Merge branch 'o-bringup' 2017-06-08 17:30:10 -07:00
Cameron Gutman 82e5aa122d Update common with FEC and latency fixes 2017-06-07 23:17:06 -07:00
Cameron Gutman fe237d1da3 Fix some exceptions that escaped on decoder shutdown and surface loss 2017-06-07 20:01:09 -07:00
Cameron Gutman e199fcd2d9 Try allowing decoder exceptions after initial start since we shouldn't throw on stop anymore 2017-06-06 22:50:08 -07:00
Cameron Gutman d7c6f63592 Force Qualcomm and Samsung HEVC decoders disabled to avoid crashes and poor performance 2017-06-06 22:49:09 -07:00
Cameron Gutman 4b9c6b149a Remove the decoder stop hack and try to workaround the issue differently 2017-06-06 22:48:28 -07:00
Cameron Gutman d1e41e41a1 Stop the connection in onStop() to try to avoid deadlocks due to surface loss. Also avoid calling stopConnection() from connection listener callbacks due to deadlock risk. 2017-06-05 20:33:23 -07:00
Cameron Gutman 96dfe25a14 Support packet size adjustments on LANs 2017-06-03 11:51:35 -07:00
Cameron Gutman f76d78607a Improve HEVC decoder compatibility by submitting VPS+SPS+PPS in one CSD blob rather than individually 2017-06-03 11:46:29 -07:00
Cameron Gutman 90a1e68c68 Move input capture check to not mask touch events 2017-06-02 18:17:18 -07:00
Cameron Gutman a413185085 Fix Pixel C keyboard d-pad regression due to aliasing with SOURCE_GAMEPAD 2017-05-31 21:51:01 -07:00
Cameron Gutman aa1b283570 Initial working pointer capture using onClick 2017-05-31 21:46:53 -07:00
Cameron Gutman f07c886711 Add isCapturing() method to mouse capture providers 2017-05-31 21:26:26 -07:00
Cameron Gutman e66b1ebec9 Initial pointer capture work for O 2017-05-31 20:50:47 -07:00
62 changed files with 551 additions and 179 deletions

No files matched your search

+20 -9
View File
@@ -4,23 +4,31 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion '25.0.3'
compileSdkVersion 26
buildToolsVersion '26.0.1'
defaultConfig {
minSdkVersion 16
targetSdkVersion 25
targetSdkVersion 26
versionName "5.0.1"
versionCode = 123
versionName "5.1.2"
versionCode = 130
}
flavorDimensions "root"
productFlavors {
root {
// Android O has native mouse capture, so don't show the rooted
// version to devices running O on the Play Store.
maxSdkVersion 25
applicationId "com.limelight.root"
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
}
dimension "root"
}
nonRoot {
@@ -28,6 +36,8 @@ android {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
}
dimension "root"
}
}
@@ -50,8 +60,9 @@ android {
}
dependencies {
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
compile 'org.bouncycastle:bcpkix-jdk15on:1.52'
compile project(':moonlight-common')
compile files('libs/jcodec-0.1.9-patched.jar')
implementation 'org.bouncycastle:bcprov-jdk15on:1.52'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.52'
implementation files('libs/jcodec-0.1.9-patched.jar')
implementation project(':moonlight-common')
}
+2 -1
View File
@@ -29,7 +29,8 @@
android:fullBackupContent="@xml/backup_rules"
android:isGame="true"
android:banner="@drawable/atv_banner"
android:icon="@drawable/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
<!-- Samsung multi-window support -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+67 -28
View File
@@ -171,6 +171,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.setOnGenericMotionListener(this);
streamView.setOnTouchListener(this);
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) {
return handleMotionEvent(motionEvent);
}
});
}
// Warn the user if they're on a metered connection
checkDataConnection();
@@ -200,7 +214,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the MediaCodec helper before creating the decoder
MediaCodecHelper.initializeWithContext(this);
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat);
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat, prefConfig.bitrate, prefConfig.batterySaver);
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
@@ -257,8 +271,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
getWindow().setSustainedPerformanceMode(true);
}
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
if (prefConfig.onscreenController) {
// create virtual onscreen controller
virtualController = new VirtualController(conn,
@@ -277,6 +289,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.getHolder().addCallback(this);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Capture is lost when focus is lost, so it must be requested again
// when focus is regained.
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
// Recapture the pointer if focus was regained
streamView.requestPointerCapture();
}
}
}
private void prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
@@ -420,6 +446,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
unbindService(usbDriverServiceConnection);
}
// Destroy the capture provider
inputCaptureProvider.destroy();
}
@Override
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (conn != null) {
int videoFormat = decoderRenderer.getActiveVideoFormat();
@@ -453,14 +490,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
}
@Override
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
finish();
}
@@ -671,7 +700,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
}
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
{
// This case is for mice
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
@@ -680,6 +710,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
int changedButtons = event.getButtonState() ^ lastButtonState;
// Ignore mouse input if we're not capturing from our input source
if (!inputCaptureProvider.isCapturingActive()) {
return false;
}
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
@@ -893,12 +928,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
connecting = connected = false;
conn.stop();
}
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
// Destroy the capture provider
inputCaptureProvider.destroy();
}
@Override
@@ -908,11 +937,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
spinner = null;
}
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe(stage+" failed: "+errorCode);
stopConnection();
// If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
}
});
}
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
}
@@ -920,11 +961,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void connectionTerminated(long errorCode) {
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe("Connection terminated: "+errorCode);
stopConnection();
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
@@ -991,15 +1035,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (connected) {
// HACK: Android is supposed to let you return from this function
// before throwing a fit if you access the surface again. Unfortunately,
// MediaCodec often tries to access the destroyed surface and triggers
// an IllegalStateException. To workaround this, we will invoke
// the DecoderRenderer's stop function ourselves, so it will hopefully
// happen early enough to not trigger the bug
decoderRenderer.stop();
// Let the decoder know immediately that the surface is gone
decoderRenderer.prepareForStop();
if (connected) {
stopConnection();
}
}
@@ -1,8 +1,10 @@
package com.limelight.binding.audio;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Build;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer;
@@ -12,10 +14,58 @@ public class AndroidAudioRenderer implements AudioRenderer {
private AudioTrack track;
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return new AudioTrack(AudioManager.STREAM_MUSIC,
48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
}
else {
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME);
AudioFormat format = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(48000)
.setChannelMask(channelConfig)
.build();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Use FLAG_LOW_LATENCY on L through N
if (lowLatency) {
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
.setAudioFormat(format)
.setAudioAttributes(attributesBuilder.build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(bufferSize);
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
if (lowLatency) {
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
}
return trackBuilder.build();
}
else {
return new AudioTrack(attributesBuilder.build(),
format,
bufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE);
}
}
}
@Override
public int setup(int audioConfiguration) {
int channelConfig;
int bufferSize;
int bytesPerFrame;
switch (audioConfiguration)
@@ -38,44 +88,76 @@ public class AndroidAudioRenderer implements AudioRenderer {
// do this on many devices and it lowers audio latency.
// We'll try the small buffer size first and if it fails,
// use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = bytesPerFrame * 2;
track = new AudioTrack(AudioManager.STREAM_MUSIC,
48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
try {
if (track != null) {
track.release();
}
} catch (Exception ignored) {}
for (int i = 0; i < 4; i++) {
boolean lowLatency;
int bufferSize;
// Now try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
// We will try:
// 1) Small buffer, low latency mode
// 2) Large buffer, low latency mode
// 3) Small buffer, standard mode
// 4) Large buffer, standard mode
switch (i) {
case 0:
case 1:
lowLatency = true;
break;
case 2:
case 3:
lowLatency = false;
break;
default:
// Unreachable
throw new IllegalStateException();
}
switch (i) {
case 0:
case 2:
bufferSize = bytesPerFrame * 2;
break;
case 1:
case 3:
// Try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
bytesPerFrame * 2);
bytesPerFrame * 2);
// Round to next frame
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
// Round to next frame
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
break;
default:
// Unreachable
throw new IllegalStateException();
}
track = new AudioTrack(AudioManager.STREAM_MUSIC,
48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
// Skip low latency options if hardware sample rate isn't 48000Hz
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
continue;
}
try {
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
track.play();
// Successfully created working AudioTrack. We're done here.
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
break;
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
try {
if (track != null) {
track.release();
track = null;
}
} catch (Exception ignored) {}
}
}
LimeLog.info("Audio track buffer size: "+bufferSize);
return 0;
}
@@ -63,6 +63,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
int[] ids = InputDevice.getDeviceIds();
for (int id : ids) {
InputDevice dev = InputDevice.getDevice(id);
if (dev == null) {
// This device was removed during enumeration
continue;
}
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure
@@ -0,0 +1,53 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
@TargetApi(Build.VERSION_CODES.O)
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
private View targetView;
public AndroidNativePointerCaptureProvider(View targetView) {
this.targetView = targetView;
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
@Override
public void enableCapture() {
super.enableCapture();
targetView.requestPointerCapture();
}
@Override
public void disableCapture() {
super.disableCapture();
targetView.releasePointerCapture();
}
@Override
public boolean isCapturingActive() {
return targetView.hasPointerCapture();
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getX();
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getY();
}
}
@@ -10,11 +10,11 @@ import android.view.View;
import android.view.ViewGroup;
@TargetApi(Build.VERSION_CODES.N)
public class AndroidCaptureProvider extends InputCaptureProvider {
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
private ViewGroup rootViewGroup;
private Context context;
public AndroidCaptureProvider(Activity activity) {
public AndroidPointerIconCaptureProvider(Activity activity) {
this.context = activity;
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
}
@@ -33,11 +33,13 @@ public class AndroidCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {
super.enableCapture();
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
}
@Override
public void disableCapture() {
super.disableCapture();
setPointerIconOnAllViews(null);
}
@@ -3,15 +3,17 @@ package com.limelight.binding.input.capture;
import android.app.Activity;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
import com.limelight.binding.input.evdev.EvdevListener;
public class InputCaptureManager {
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
// Shield capture is preferred because it can capture when the cursor is over
// the system UI. Android N native capture can only capture over views owned
// by the application.
if (ShieldCaptureProvider.isCaptureProviderSupported()) {
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android O+ native mouse capture");
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
}
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity);
}
@@ -19,9 +21,11 @@ public class InputCaptureManager {
LimeLog.info("Using Evdev mouse capture");
return new EvdevCaptureProvider(activity, rootListener);
}
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android N+ native mouse capture");
return new AndroidCaptureProvider(activity);
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
// Android N's native capture can't capture over system UI elements
// so we want to only use it if there's no other option.
LimeLog.info("Using Android N+ pointer hiding");
return new AndroidPointerIconCaptureProvider(activity);
}
else {
LimeLog.info("Mouse capture not available");
@@ -3,10 +3,25 @@ package com.limelight.binding.input.capture;
import android.view.MotionEvent;
public abstract class InputCaptureProvider {
public abstract void enableCapture();
public abstract void disableCapture();
protected boolean isCapturing;
public void enableCapture() {
isCapturing = true;
}
public void disableCapture() {
isCapturing = false;
}
public void destroy() {}
public boolean isCapturingEnabled() {
return isCapturing;
}
public boolean isCapturingActive() {
return isCapturing;
}
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return false;
}
@@ -1,10 +1,4 @@
package com.limelight.binding.input.capture;
public class NullCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {}
@Override
public void disableCapture() {}
}
public class NullCaptureProvider extends InputCaptureProvider {}
@@ -63,11 +63,13 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {
super.enableCapture();
setCursorVisibility(false);
}
@Override
public void disableCapture() {
super.disableCapture();
setCursorVisibility(true);
}
@@ -221,6 +221,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {
super.enableCapture();
if (!started) {
// Start the handler thread if it's our first time
// capturing
@@ -247,6 +248,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
@Override
public void disableCapture() {
super.disableCapture();
// This may be called on the main thread
runInNetworkSafeContextSynchronously(new Runnable() {
@Override
@@ -23,17 +23,7 @@ import android.view.SurfaceHolder;
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private static final boolean USE_FRAME_RENDER_TIME = false;
// This value is added to the PTS for each frame to force frames delivered right at
// V-sync time to be delayed until the next V-sync rather than dropping the existing frame.
// To make matters worse, these frames often end up being displayed for 2 V-syncs because
// the next frame isn't ready at the next V-sync. So without this, we end up with a pattern of:
// F0 - Displayed | F1 - Dropped | F2 - Displayed | F2 - Displayed again | F3 - Displayed ...
// which can be very noticeable for some video content and users.
//
// This value is currently 6 ms, which means that a frame ready for display less than 6 ms from
// the next V-sync will be delayed until the following V-sync to ensure smooth frame pacing.
private static final int PTS_OFFSET_NS = 6 * 1000000;
private static final boolean FRAME_RENDER_TIME_ONLY = USE_FRAME_RENDER_TIME && false;
// Used on versions < 5.0
private ByteBuffer[] legacyInputBuffers;
@@ -41,6 +31,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private MediaCodecInfo avcDecoder;
private MediaCodecInfo hevcDecoder;
// Used for HEVC only
private byte[] vpsBuffer;
private byte[] spsBuffer;
private MediaCodec videoDecoder;
private Thread rendererThread;
private Thread[] spinnerThreads;
@@ -51,16 +45,25 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private boolean refFrameInvalidationActive;
private int initialWidth, initialHeight;
private int videoFormat;
private Object renderTarget;
private boolean stopping;
private SurfaceHolder renderTarget;
private volatile boolean stopping;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
private RendererException initialException;
private long initialExceptionTimestamp;
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
private long lastTimestampUs;
private long decoderTimeMs;
private long totalTimeMs;
private int totalFrames;
private int frameLossEvents;
private int framesLost;
private int lastFrameNumber;
private int refreshRate;
private int bitrate;
private int numSpsIn;
private int numPpsIn;
@@ -102,14 +105,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return decoderInfo;
}
public void setRenderTarget(Object renderTarget) {
public void setRenderTarget(SurfaceHolder renderTarget) {
this.renderTarget = renderTarget;
}
public MediaCodecDecoderRenderer(int videoFormat) {
public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver) {
//dumpDecoders();
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
this.bitrate = bitrate;
// Disable spinner threads in battery saver mode
if (batterySaver) {
spinnerThreads = new Thread[0];
}
else {
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
}
avcDecoder = findAvcDecoder();
if (avcDecoder != null) {
@@ -167,6 +178,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
this.initialWidth = width;
this.initialHeight = height;
this.videoFormat = format;
this.refreshRate = redrawRate;
String mimeType;
String selectedDecoderName;
@@ -244,7 +256,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
try {
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -277,12 +289,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return 0;
}
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
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;
if (codecExc.isTransient()) {
if (codecExc.isTransient() && !throwOnTransient) {
// We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo());
return;
@@ -292,14 +304,31 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
}
// Only throw if this happens at the beginning of a stream
// but not if we're stopping
if (totalFrames > 0 && totalFrames < 20 && !stopping) {
if (buf != null || codecFlags != 0) {
throw new RendererException(this, e, buf, codecFlags);
// Only throw if we're not stopping
if (!stopping) {
//
// There seems to be a race condition with decoder/surface teardown causing some
// decoders to to throw IllegalStateExceptions even before 'stopping' is set.
// To workaround this while allowing real exceptions to propagate, we will eat the
// first exception. If we are still receiving exceptions 3 seconds later, we will
// throw the original exception again.
//
if (initialException != null) {
// This isn't the first time we've had an exception processing video
if (System.currentTimeMillis() - initialExceptionTimestamp >= EXCEPTION_REPORT_DELAY_MS) {
// It's been over 3 seconds and we're still getting exceptions. Throw the original now.
throw initialException;
}
}
else {
throw new RendererException(this, e);
// 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);
}
initialExceptionTimestamp = System.currentTimeMillis();
}
}
}
@@ -310,7 +339,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@Override
public void run() {
BufferInfo info = new BufferInfo();
while (!isInterrupted()) {
while (!stopping) {
try {
// Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
@@ -327,17 +356,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
// Render the last buffer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// On L and later, we can fine tune our PTS right before rendering, so
// we take advantage of that for frame pacing using the PTS offset. Using
// this version of releaseOutputBuffer() also lets SurfaceFlinger drop extra
// frames in the SurfaceView<->SurfaceFlinger BufferQueue that reduces
// latency if an stray extra frame ends up in the output queue.
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime() + PTS_OFFSET_NS);
}
else {
videoDecoder.releaseOutputBuffer(lastIndex, true);
}
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
@@ -360,7 +379,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
handleDecoderException(e, null, 0, false);
}
}
}
@@ -378,7 +397,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
public void run() {
// This thread exists to keep the CPU at a higher DVFS state on devices
// where the governor scales clock speed sporadically, causing dropped frames.
while (!isInterrupted()) {
while (!stopping) {
try {
Thread.sleep(0, 1);
} catch (InterruptedException e) {
@@ -400,11 +419,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
startTime = MediaCodecHelper.getMonotonicMillis();
try {
while (rendererThread.isAlive() && index < 0 && !stopping) {
while (index < 0 && !stopping) {
index = videoDecoder.dequeueInputBuffer(10000);
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
handleDecoderException(e, null, 0, true);
return MediaCodec.INFO_TRY_AGAIN_LATER;
}
@@ -427,16 +446,41 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
startSpinnerThreads();
}
@Override
public void stop() {
// !!! May be called even if setup()/start() fails !!!
public void prepareForStop() {
// Let the decoding code know to ignore codec exceptions now
stopping = true;
// Halt the rendering thread
rendererThread.interrupt();
if (rendererThread != null) {
rendererThread.interrupt();
}
}
@Override
public void stop() {
// May be called already, but we'll call it now to be safe
prepareForStop();
try {
// Invalidate pending decode buffers
videoDecoder.flush();
} catch (Exception e) {
e.printStackTrace();
}
// Wait for the renderer thread to shut down
try {
rendererThread.join();
} catch (InterruptedException ignored) { }
try {
// Stop the video decoder
videoDecoder.stop();
} catch (Exception e) {
e.printStackTrace();
}
// Halt the spinner threads
for (Thread t : spinnerThreads) {
t.interrupt();
@@ -454,31 +498,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
// Try 25 times to submit the input buffer before throwing a real exception
int i;
Exception lastException = null;
for (i = 0; i < 25; i++) {
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
offset, length,
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(e, null, codecFlags);
lastException = e;
}
}
if (i == 25 && totalFrames > 0 && totalFrames < 20 && !stopping) {
throw new RendererException(this, lastException, null, codecFlags);
}
else if (i != 25) {
// Queued input buffer
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
offset, length,
timestampUs, codecFlags);
return true;
}
else {
// Failed to queue
} catch (Exception e) {
handleDecoderException(e, null, codecFlags, true);
return false;
}
}
@@ -489,7 +515,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
ByteBuffer buf;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
buf = videoDecoder.getInputBuffer(inputBufferIndex);
try {
buf = videoDecoder.getInputBuffer(inputBufferIndex);
} catch (Exception e) {
handleDecoderException(e, null, 0, true);
return null;
}
}
else {
buf = legacyInputBuffers[inputBufferIndex];
@@ -520,25 +551,36 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@SuppressWarnings("deprecation")
@Override
public int submitDecodeUnit(byte[] frameData, int frameLength) {
public int submitDecodeUnit(byte[] frameData, int frameLength, int frameNumber, long receiveTimeMs) {
totalFrames++;
// We can receive the same "frame" multiple times if it's an IDR frame.
// In that case, each frame start NALU is submitted independently.
if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
framesLost += frameNumber - lastFrameNumber - 1;
frameLossEvents++;
}
lastFrameNumber = frameNumber;
int inputBufferIndex;
ByteBuffer buf;
long timestampUs = System.nanoTime() / 1000;
if (!FRAME_RENDER_TIME_ONLY) {
// Count time from first packet received to decode start
totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
}
if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing
timestampUs = lastTimestampUs + 1;
}
lastTimestampUs = timestampUs;
int inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_OK;
}
ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex);
int codecFlags = 0;
boolean needsSpsReplay = false;
@@ -640,6 +682,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// Patch the SPS constraint flags
doProfileSpecificSpsPatching(sps);
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// Write the annex B header
buf.put(frameData, 0, 5);
@@ -662,6 +716,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
numPpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
if (needsBaselineSpsHack) {
LimeLog.info("Saw PPS; disabling SPS hack");
@@ -673,22 +738,66 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
else if (frameData[4] == 0x40) {
numVpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
// Batch this to submit together with SPS and PPS per AOSP docs
vpsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, vpsBuffer, 0, frameLength);
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x42) {
numSpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
// Batch this to submit together with VPS and PPS per AOSP docs
spsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, spsBuffer, 0, frameLength);
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x44) {
numPpsIn++;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// When we get the PPS, submit the VPS and SPS together with
// the PPS, as required by AOSP docs on use of HEVC and MediaCodec.
if (vpsBuffer != null) {
buf.put(vpsBuffer);
}
if (spsBuffer != null) {
buf.put(spsBuffer);
}
// This is the HEVC CSD blob
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
}
else {
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
}
// Copy data from our buffer list into the input buffer
buf.put(frameData, 0, frameLength);
if (!queueInputBuffer(inputBufferIndex,
0, frameLength,
0, buf.position(),
timestampUs, codecFlags)) {
return MoonBridge.DR_NEED_IDR;
}
@@ -711,6 +820,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
if (inputBuffer == null) {
return false;
}
// Write the Annex B header
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
@@ -800,8 +912,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.bitrate+" Mbps \n";
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n";
str += "Frame losses: "+renderer.framesLost+" in "+frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
@@ -63,15 +63,24 @@ public class MediaCodecHelper {
static {
blacklistedDecoderPrefixes = new LinkedList<>();
// Software decoders that don't support H264 high profile
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
// Blacklist software decoders that don't support H264 high profile,
// but exclude the official AOSP emulator from this restriction.
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
// decoder. While not strictly necessary, I'm going to fully blacklist this
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
// Force these decoders disabled because:
// 1) They are software decoders, so the performance is terrible
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
}
static {
@@ -98,6 +107,11 @@ public class MediaCodecHelper {
static {
whitelistedHevcDecoders = new LinkedList<>();
// Allow software HEVC decoding in the official AOSP emulator
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
whitelistedHevcDecoders.add("omx.google");
}
// Exynos seems to be the only HEVC decoder that works reliably
whitelistedHevcDecoders.add("omx.exynos");
@@ -23,6 +23,7 @@ public class PreferenceConfiguration {
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -45,6 +46,7 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto";
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
private static final boolean DEFAULT_BATTERY_SAVER = false;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
@@ -58,6 +60,7 @@ public class PreferenceConfiguration {
public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean onscreenController;
public boolean batterySaver;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
@@ -188,6 +191,7 @@ public class PreferenceConfiguration {
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
return config;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_pc_scut_background"/>
<foreground android:drawable="@mipmap/ic_pc_scut_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_pc_scut_background"/>
<foreground android:drawable="@mipmap/ic_pc_scut_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#565C64</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_pc_scut_background">#FFFFFF</color>
</resources>
+2
View File
@@ -104,6 +104,8 @@
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="title_checkbox_battery_saver">Battery saver</string>
<string name="summary_checkbox_battery_saver">Uses less battery, but may increase stuttering</string>
<string name="category_audio_settings">Audio Settings</string>
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
+3 -3
View File
@@ -21,9 +21,9 @@
android:title="@string/title_checkbox_stretch_video"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_disable_warnings"
android:title="@string/title_checkbox_disable_warnings"
android:summary="@string/summary_checkbox_disable_warnings"
android:key="checkbox_battery_saver"
android:title="@string/title_checkbox_battery_saver"
android:summary="@string/summary_checkbox_battery_saver"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_audio_settings">
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.2'
classpath 'com.android.tools.build:gradle:3.0.0-beta2'
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
#Thu Mar 02 18:32:01 PST 2017
#Sat Aug 12 15:52:56 PDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-rc-1-all.zip