Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ac721a35b | ||
|
|
e49b1c92a2 | ||
|
|
db4295bf83 | ||
|
|
824c37f9d5 | ||
|
|
acf4426952 | ||
|
|
e8c50342ab | ||
|
|
598995de3b | ||
|
|
01cf0cc649 | ||
|
|
fa560f462f | ||
|
|
f6e40118a9 | ||
|
|
fe7148dbd4 | ||
|
|
60de065836 | ||
|
|
6f82f82abb | ||
|
|
42f18cb4ac | ||
|
|
1bbd0054c2 | ||
|
|
acdde37a3a | ||
|
|
ad40e12167 | ||
|
|
1b3322b5ee | ||
|
|
6340ec6c6d | ||
|
|
babd92c8c0 | ||
|
|
7f1fe5f520 | ||
|
|
01458770d2 | ||
|
|
8d05f044f5 | ||
|
|
f5680b59a5 | ||
|
|
0ecf86c7ed | ||
|
|
6789e8d497 | ||
|
|
7d0160d556 | ||
|
|
f6a0990432 | ||
|
|
5d6094df97 | ||
|
|
d98d4aeda2 | ||
|
|
852dcf5a2d | ||
|
|
82e5aa122d | ||
|
|
fe237d1da3 | ||
|
|
e199fcd2d9 | ||
|
|
d7c6f63592 | ||
|
|
4b9c6b149a | ||
|
|
d1e41e41a1 | ||
|
|
96dfe25a14 | ||
|
|
f76d78607a | ||
|
|
90a1e68c68 | ||
|
|
a413185085 | ||
|
|
aa1b283570 | ||
|
|
f07c886711 | ||
|
|
e66b1ebec9 |
No files matched your search
@@ -4,23 +4,31 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 25
|
compileSdkVersion 26
|
||||||
buildToolsVersion '25.0.3'
|
buildToolsVersion '26.0.1'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 26
|
||||||
|
|
||||||
versionName "5.0.1"
|
versionName "5.1.2"
|
||||||
versionCode = 123
|
versionCode = 130
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "root"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
root {
|
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"
|
applicationId "com.limelight.root"
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
@@ -28,6 +36,8 @@ android {
|
|||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +60,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.52'
|
||||||
compile 'org.bouncycastle:bcpkix-jdk15on:1.52'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.52'
|
||||||
compile project(':moonlight-common')
|
implementation files('libs/jcodec-0.1.9-patched.jar')
|
||||||
compile files('libs/jcodec-0.1.9-patched.jar')
|
|
||||||
|
implementation project(':moonlight-common')
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:isGame="true"
|
android:isGame="true"
|
||||||
android:banner="@drawable/atv_banner"
|
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">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<!-- Samsung multi-window support -->
|
<!-- Samsung multi-window support -->
|
||||||
|
|||||||
|
After Width: | Height: | Size: 58 KiB |
@@ -171,6 +171,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
streamView.setOnGenericMotionListener(this);
|
streamView.setOnGenericMotionListener(this);
|
||||||
streamView.setOnTouchListener(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
|
// Warn the user if they're on a metered connection
|
||||||
checkDataConnection();
|
checkDataConnection();
|
||||||
|
|
||||||
@@ -200,7 +214,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Initialize the MediaCodec helper before creating the decoder
|
// Initialize the MediaCodec helper before creating the decoder
|
||||||
MediaCodecHelper.initializeWithContext(this);
|
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
|
// 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()) {
|
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
||||||
@@ -257,8 +271,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
getWindow().setSustainedPerformanceMode(true);
|
getWindow().setSustainedPerformanceMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
|
||||||
|
|
||||||
if (prefConfig.onscreenController) {
|
if (prefConfig.onscreenController) {
|
||||||
// create virtual onscreen controller
|
// create virtual onscreen controller
|
||||||
virtualController = new VirtualController(conn,
|
virtualController = new VirtualController(conn,
|
||||||
@@ -277,6 +289,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
streamView.getHolder().addCallback(this);
|
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() {
|
private void prepareDisplayForRendering() {
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||||
@@ -420,6 +446,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
unbindService(usbDriverServiceConnection);
|
unbindService(usbDriverServiceConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destroy the capture provider
|
||||||
|
inputCaptureProvider.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
if (conn != null) {
|
if (conn != null) {
|
||||||
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
||||||
|
|
||||||
@@ -453,14 +490,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
SpinnerDialog.closeDialogs(this);
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@@ -671,7 +700,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
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
|
// This case is for mice
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
@@ -680,6 +710,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
{
|
{
|
||||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
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) {
|
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
||||||
// Send the vertical scroll packet
|
// Send the vertical scroll packet
|
||||||
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||||
@@ -893,12 +928,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
connecting = connected = false;
|
connecting = connected = false;
|
||||||
conn.stop();
|
conn.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable cursor visibility again
|
|
||||||
inputCaptureProvider.disableCapture();
|
|
||||||
|
|
||||||
// Destroy the capture provider
|
|
||||||
inputCaptureProvider.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -908,11 +937,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
spinner = null;
|
spinner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable cursor visibility again
|
||||||
|
inputCaptureProvider.disableCapture();
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
LimeLog.severe(stage+" failed: "+errorCode);
|
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),
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||||
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
||||||
}
|
}
|
||||||
@@ -920,11 +961,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionTerminated(long errorCode) {
|
public void connectionTerminated(long errorCode) {
|
||||||
|
// Enable cursor visibility again
|
||||||
|
inputCaptureProvider.disableCapture();
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
LimeLog.severe("Connection terminated: "+errorCode);
|
LimeLog.severe("Connection terminated: "+errorCode);
|
||||||
|
|
||||||
stopConnection();
|
stopConnection();
|
||||||
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
||||||
getResources().getString(R.string.conn_terminated_msg), true);
|
getResources().getString(R.string.conn_terminated_msg), true);
|
||||||
}
|
}
|
||||||
@@ -991,15 +1035,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
if (connected) {
|
// Let the decoder know immediately that the surface is gone
|
||||||
// HACK: Android is supposed to let you return from this function
|
decoderRenderer.prepareForStop();
|
||||||
// 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();
|
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
stopConnection();
|
stopConnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.limelight.binding.audio;
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
|
import android.media.AudioAttributes;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
@@ -12,10 +14,58 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
|
|
||||||
private AudioTrack track;
|
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
|
@Override
|
||||||
public int setup(int audioConfiguration) {
|
public int setup(int audioConfiguration) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bufferSize;
|
|
||||||
int bytesPerFrame;
|
int bytesPerFrame;
|
||||||
|
|
||||||
switch (audioConfiguration)
|
switch (audioConfiguration)
|
||||||
@@ -38,44 +88,76 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
// do this on many devices and it lowers audio latency.
|
// do this on many devices and it lowers audio latency.
|
||||||
// We'll try the small buffer size first and if it fails,
|
// We'll try the small buffer size first and if it fails,
|
||||||
// use the recommended larger buffer size.
|
// use the recommended larger buffer size.
|
||||||
try {
|
|
||||||
// Buffer two frames of audio if possible
|
|
||||||
bufferSize = bytesPerFrame * 2;
|
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
for (int i = 0; i < 4; i++) {
|
||||||
48000,
|
boolean lowLatency;
|
||||||
channelConfig,
|
int bufferSize;
|
||||||
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) {}
|
|
||||||
|
|
||||||
// Now try the larger buffer size
|
// We will try:
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
// 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,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
bytesPerFrame * 2);
|
bytesPerFrame * 2);
|
||||||
|
|
||||||
// Round to next frame
|
// Round to next frame
|
||||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unreachable
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
// Skip low latency options if hardware sample rate isn't 48000Hz
|
||||||
48000,
|
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
||||||
channelConfig,
|
continue;
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
}
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
try {
|
||||||
track.play();
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
int[] ids = InputDevice.getDeviceIds();
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
InputDevice dev = InputDevice.getDevice(id);
|
InputDevice dev = InputDevice.getDevice(id);
|
||||||
|
if (dev == null) {
|
||||||
|
// This device was removed during enumeration
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
||||||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
||||||
// This looks like a gamepad, but we'll check X and Y to be sure
|
// 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;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
public class AndroidCaptureProvider extends InputCaptureProvider {
|
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
||||||
private ViewGroup rootViewGroup;
|
private ViewGroup rootViewGroup;
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
public AndroidCaptureProvider(Activity activity) {
|
public AndroidPointerIconCaptureProvider(Activity activity) {
|
||||||
this.context = activity;
|
this.context = activity;
|
||||||
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||||
}
|
}
|
||||||
@@ -33,11 +33,13 @@ public class AndroidCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableCapture() {
|
public void enableCapture() {
|
||||||
|
super.enableCapture();
|
||||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableCapture() {
|
public void disableCapture() {
|
||||||
|
super.disableCapture();
|
||||||
setPointerIconOnAllViews(null);
|
setPointerIconOnAllViews(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3,15 +3,17 @@ package com.limelight.binding.input.capture;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.R;
|
||||||
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
|
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
|
||||||
public class InputCaptureManager {
|
public class InputCaptureManager {
|
||||||
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||||
// Shield capture is preferred because it can capture when the cursor is over
|
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
||||||
// the system UI. Android N native capture can only capture over views owned
|
LimeLog.info("Using Android O+ native mouse capture");
|
||||||
// by the application.
|
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
||||||
if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
}
|
||||||
|
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||||
LimeLog.info("Using NVIDIA mouse capture extension");
|
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||||
return new ShieldCaptureProvider(activity);
|
return new ShieldCaptureProvider(activity);
|
||||||
}
|
}
|
||||||
@@ -19,9 +21,11 @@ public class InputCaptureManager {
|
|||||||
LimeLog.info("Using Evdev mouse capture");
|
LimeLog.info("Using Evdev mouse capture");
|
||||||
return new EvdevCaptureProvider(activity, rootListener);
|
return new EvdevCaptureProvider(activity, rootListener);
|
||||||
}
|
}
|
||||||
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
|
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
||||||
LimeLog.info("Using Android N+ native mouse capture");
|
// Android N's native capture can't capture over system UI elements
|
||||||
return new AndroidCaptureProvider(activity);
|
// 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 {
|
else {
|
||||||
LimeLog.info("Mouse capture not available");
|
LimeLog.info("Mouse capture not available");
|
||||||
|
|||||||
@@ -3,10 +3,25 @@ package com.limelight.binding.input.capture;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
public abstract class InputCaptureProvider {
|
public abstract class InputCaptureProvider {
|
||||||
public abstract void enableCapture();
|
protected boolean isCapturing;
|
||||||
public abstract void disableCapture();
|
|
||||||
|
public void enableCapture() {
|
||||||
|
isCapturing = true;
|
||||||
|
}
|
||||||
|
public void disableCapture() {
|
||||||
|
isCapturing = false;
|
||||||
|
}
|
||||||
|
|
||||||
public void destroy() {}
|
public void destroy() {}
|
||||||
|
|
||||||
|
public boolean isCapturingEnabled() {
|
||||||
|
return isCapturing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCapturingActive() {
|
||||||
|
return isCapturing;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
package com.limelight.binding.input.capture;
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
|
||||||
public class NullCaptureProvider extends InputCaptureProvider {
|
public class NullCaptureProvider extends InputCaptureProvider {}
|
||||||
@Override
|
|
||||||
public void enableCapture() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {}
|
|
||||||
}
|
|
||||||
@@ -63,11 +63,13 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableCapture() {
|
public void enableCapture() {
|
||||||
|
super.enableCapture();
|
||||||
setCursorVisibility(false);
|
setCursorVisibility(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableCapture() {
|
public void disableCapture() {
|
||||||
|
super.disableCapture();
|
||||||
setCursorVisibility(true);
|
setCursorVisibility(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableCapture() {
|
public void enableCapture() {
|
||||||
|
super.enableCapture();
|
||||||
if (!started) {
|
if (!started) {
|
||||||
// Start the handler thread if it's our first time
|
// Start the handler thread if it's our first time
|
||||||
// capturing
|
// capturing
|
||||||
@@ -247,6 +248,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableCapture() {
|
public void disableCapture() {
|
||||||
|
super.disableCapture();
|
||||||
// This may be called on the main thread
|
// This may be called on the main thread
|
||||||
runInNetworkSafeContextSynchronously(new Runnable() {
|
runInNetworkSafeContextSynchronously(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -23,17 +23,7 @@ import android.view.SurfaceHolder;
|
|||||||
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||||
|
|
||||||
private static final boolean USE_FRAME_RENDER_TIME = false;
|
private static final boolean USE_FRAME_RENDER_TIME = false;
|
||||||
|
private static final boolean FRAME_RENDER_TIME_ONLY = 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;
|
|
||||||
|
|
||||||
// Used on versions < 5.0
|
// Used on versions < 5.0
|
||||||
private ByteBuffer[] legacyInputBuffers;
|
private ByteBuffer[] legacyInputBuffers;
|
||||||
@@ -41,6 +31,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private MediaCodecInfo avcDecoder;
|
private MediaCodecInfo avcDecoder;
|
||||||
private MediaCodecInfo hevcDecoder;
|
private MediaCodecInfo hevcDecoder;
|
||||||
|
|
||||||
|
// Used for HEVC only
|
||||||
|
private byte[] vpsBuffer;
|
||||||
|
private byte[] spsBuffer;
|
||||||
|
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private Thread[] spinnerThreads;
|
private Thread[] spinnerThreads;
|
||||||
@@ -51,16 +45,25 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private boolean refFrameInvalidationActive;
|
private boolean refFrameInvalidationActive;
|
||||||
private int initialWidth, initialHeight;
|
private int initialWidth, initialHeight;
|
||||||
private int videoFormat;
|
private int videoFormat;
|
||||||
private Object renderTarget;
|
private SurfaceHolder renderTarget;
|
||||||
private boolean stopping;
|
private volatile boolean stopping;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
private SeqParameterSet savedSps;
|
private SeqParameterSet savedSps;
|
||||||
|
|
||||||
|
private RendererException initialException;
|
||||||
|
private long initialExceptionTimestamp;
|
||||||
|
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
|
||||||
|
|
||||||
private long lastTimestampUs;
|
private long lastTimestampUs;
|
||||||
private long decoderTimeMs;
|
private long decoderTimeMs;
|
||||||
private long totalTimeMs;
|
private long totalTimeMs;
|
||||||
private int totalFrames;
|
private int totalFrames;
|
||||||
|
private int frameLossEvents;
|
||||||
|
private int framesLost;
|
||||||
|
private int lastFrameNumber;
|
||||||
|
private int refreshRate;
|
||||||
|
private int bitrate;
|
||||||
|
|
||||||
private int numSpsIn;
|
private int numSpsIn;
|
||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
@@ -102,14 +105,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return decoderInfo;
|
return decoderInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRenderTarget(Object renderTarget) {
|
public void setRenderTarget(SurfaceHolder renderTarget) {
|
||||||
this.renderTarget = renderTarget;
|
this.renderTarget = renderTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaCodecDecoderRenderer(int videoFormat) {
|
public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver) {
|
||||||
//dumpDecoders();
|
//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();
|
avcDecoder = findAvcDecoder();
|
||||||
if (avcDecoder != null) {
|
if (avcDecoder != null) {
|
||||||
@@ -167,6 +178,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
this.initialWidth = width;
|
this.initialWidth = width;
|
||||||
this.initialHeight = height;
|
this.initialHeight = height;
|
||||||
this.videoFormat = format;
|
this.videoFormat = format;
|
||||||
|
this.refreshRate = redrawRate;
|
||||||
|
|
||||||
String mimeType;
|
String mimeType;
|
||||||
String selectedDecoderName;
|
String selectedDecoderName;
|
||||||
@@ -244,7 +256,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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);
|
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||||
|
|
||||||
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@@ -277,12 +289,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return 0;
|
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if (e instanceof CodecException) {
|
if (e instanceof CodecException) {
|
||||||
CodecException codecExc = (CodecException) e;
|
CodecException codecExc = (CodecException) e;
|
||||||
|
|
||||||
if (codecExc.isTransient()) {
|
if (codecExc.isTransient() && !throwOnTransient) {
|
||||||
// We'll let transient exceptions go
|
// We'll let transient exceptions go
|
||||||
LimeLog.warning(codecExc.getDiagnosticInfo());
|
LimeLog.warning(codecExc.getDiagnosticInfo());
|
||||||
return;
|
return;
|
||||||
@@ -292,14 +304,31 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only throw if this happens at the beginning of a stream
|
// Only throw if we're not stopping
|
||||||
// but not if we're stopping
|
if (!stopping) {
|
||||||
if (totalFrames > 0 && totalFrames < 20 && !stopping) {
|
//
|
||||||
if (buf != null || codecFlags != 0) {
|
// There seems to be a race condition with decoder/surface teardown causing some
|
||||||
throw new RendererException(this, e, buf, codecFlags);
|
// 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 {
|
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
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
while (!isInterrupted()) {
|
while (!stopping) {
|
||||||
try {
|
try {
|
||||||
// Try to output a frame
|
// Try to output a frame
|
||||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
||||||
@@ -327,17 +356,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the last buffer
|
// Render the last buffer
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||||
@@ -360,7 +379,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(e, null, 0);
|
handleDecoderException(e, null, 0, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,7 +397,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
public void run() {
|
public void run() {
|
||||||
// This thread exists to keep the CPU at a higher DVFS state on devices
|
// This thread exists to keep the CPU at a higher DVFS state on devices
|
||||||
// where the governor scales clock speed sporadically, causing dropped frames.
|
// where the governor scales clock speed sporadically, causing dropped frames.
|
||||||
while (!isInterrupted()) {
|
while (!stopping) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(0, 1);
|
Thread.sleep(0, 1);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@@ -400,11 +419,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
startTime = MediaCodecHelper.getMonotonicMillis();
|
startTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (rendererThread.isAlive() && index < 0 && !stopping) {
|
while (index < 0 && !stopping) {
|
||||||
index = videoDecoder.dequeueInputBuffer(10000);
|
index = videoDecoder.dequeueInputBuffer(10000);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(e, null, 0);
|
handleDecoderException(e, null, 0, true);
|
||||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,16 +446,41 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
startSpinnerThreads();
|
startSpinnerThreads();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// !!! May be called even if setup()/start() fails !!!
|
||||||
public void stop() {
|
public void prepareForStop() {
|
||||||
|
// Let the decoding code know to ignore codec exceptions now
|
||||||
stopping = true;
|
stopping = true;
|
||||||
|
|
||||||
// Halt the rendering thread
|
// 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 {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException ignored) { }
|
} catch (InterruptedException ignored) { }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stop the video decoder
|
||||||
|
videoDecoder.stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
// Halt the spinner threads
|
// Halt the spinner threads
|
||||||
for (Thread t : spinnerThreads) {
|
for (Thread t : spinnerThreads) {
|
||||||
t.interrupt();
|
t.interrupt();
|
||||||
@@ -454,31 +498,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
|
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
|
try {
|
||||||
int i;
|
videoDecoder.queueInputBuffer(inputBufferIndex,
|
||||||
Exception lastException = null;
|
offset, length,
|
||||||
|
timestampUs, codecFlags);
|
||||||
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
|
|
||||||
return true;
|
return true;
|
||||||
}
|
} catch (Exception e) {
|
||||||
else {
|
handleDecoderException(e, null, codecFlags, true);
|
||||||
// Failed to queue
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,7 +515,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
ByteBuffer buf;
|
ByteBuffer buf;
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
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 {
|
else {
|
||||||
buf = legacyInputBuffers[inputBufferIndex];
|
buf = legacyInputBuffers[inputBufferIndex];
|
||||||
@@ -520,25 +551,36 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public int submitDecodeUnit(byte[] frameData, int frameLength) {
|
public int submitDecodeUnit(byte[] frameData, int frameLength, int frameNumber, long receiveTimeMs) {
|
||||||
totalFrames++;
|
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;
|
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) {
|
if (timestampUs <= lastTimestampUs) {
|
||||||
// We can't submit multiple buffers with the same timestamp
|
// We can't submit multiple buffers with the same timestamp
|
||||||
// so bump it up by one before queuing
|
// so bump it up by one before queuing
|
||||||
timestampUs = lastTimestampUs + 1;
|
timestampUs = lastTimestampUs + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastTimestampUs = timestampUs;
|
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;
|
int codecFlags = 0;
|
||||||
boolean needsSpsReplay = false;
|
boolean needsSpsReplay = false;
|
||||||
|
|
||||||
@@ -640,6 +682,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
// Patch the SPS constraint flags
|
// Patch the SPS constraint flags
|
||||||
doProfileSpecificSpsPatching(sps);
|
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
|
// Write the annex B header
|
||||||
buf.put(frameData, 0, 5);
|
buf.put(frameData, 0, 5);
|
||||||
|
|
||||||
@@ -662,6 +716,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
numPpsIn++;
|
numPpsIn++;
|
||||||
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
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) {
|
if (needsBaselineSpsHack) {
|
||||||
LimeLog.info("Saw PPS; disabling SPS hack");
|
LimeLog.info("Saw PPS; disabling SPS hack");
|
||||||
@@ -673,22 +738,66 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
else if (frameData[4] == 0x40) {
|
else if (frameData[4] == 0x40) {
|
||||||
numVpsIn++;
|
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) {
|
else if (frameData[4] == 0x42) {
|
||||||
numSpsIn++;
|
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) {
|
else if (frameData[4] == 0x44) {
|
||||||
numPpsIn++;
|
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;
|
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
|
// Copy data from our buffer list into the input buffer
|
||||||
buf.put(frameData, 0, frameLength);
|
buf.put(frameData, 0, frameLength);
|
||||||
|
|
||||||
if (!queueInputBuffer(inputBufferIndex,
|
if (!queueInputBuffer(inputBufferIndex,
|
||||||
0, frameLength,
|
0, buf.position(),
|
||||||
timestampUs, codecFlags)) {
|
timestampUs, codecFlags)) {
|
||||||
return MoonBridge.DR_NEED_IDR;
|
return MoonBridge.DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
@@ -711,6 +820,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
|
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
|
||||||
|
if (inputBuffer == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Write the Annex B header
|
// Write the Annex B header
|
||||||
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
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 += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
||||||
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
||||||
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\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 += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
||||||
str += "Total frames: "+renderer.totalFrames+"\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 end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
|
||||||
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
|
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
|
||||||
|
|
||||||
|
|||||||
@@ -64,14 +64,23 @@ public class MediaCodecHelper {
|
|||||||
static {
|
static {
|
||||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
// Software decoders that don't support H264 high profile
|
// Blacklist software decoders that don't support H264 high profile,
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
// but exclude the official AOSP emulator from this restriction.
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
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
|
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
|
||||||
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
||||||
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
||||||
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
|
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 {
|
static {
|
||||||
@@ -98,6 +107,11 @@ public class MediaCodecHelper {
|
|||||||
static {
|
static {
|
||||||
whitelistedHevcDecoders = new LinkedList<>();
|
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
|
// Exynos seems to be the only HEVC decoder that works reliably
|
||||||
whitelistedHevcDecoders.add("omx.exynos");
|
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 USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
||||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
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 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_30 = 5;
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
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 boolean DEFAULT_USB_DRIVER = true;
|
||||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
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 FORCE_H265_ON = -1;
|
||||||
public static final int AUTOSELECT_H265 = 0;
|
public static final int AUTOSELECT_H265 = 0;
|
||||||
@@ -58,6 +60,7 @@ public class PreferenceConfiguration {
|
|||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||||
public boolean onscreenController;
|
public boolean onscreenController;
|
||||||
|
public boolean batterySaver;
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
public static int getDefaultBitrate(String resFpsString) {
|
||||||
if (resFpsString.equals("720p30")) {
|
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.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
||||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
||||||
|
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
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>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 715 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 471 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 816 B |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
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>
|
||||||
@@ -104,6 +104,8 @@
|
|||||||
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
|
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
|
||||||
<string name="title_checkbox_disable_warnings">Disable warning messages</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="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="category_audio_settings">Audio Settings</string>
|
||||||
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
|
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
android:title="@string/title_checkbox_stretch_video"
|
android:title="@string/title_checkbox_stretch_video"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:key="checkbox_disable_warnings"
|
android:key="checkbox_battery_saver"
|
||||||
android:title="@string/title_checkbox_disable_warnings"
|
android:title="@string/title_checkbox_battery_saver"
|
||||||
android:summary="@string/summary_checkbox_disable_warnings"
|
android:summary="@string/summary_checkbox_battery_saver"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory android:title="@string/category_audio_settings">
|
<PreferenceCategory android:title="@string/category_audio_settings">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:2.3.2'
|
classpath 'com.android.tools.build:gradle:3.0.0-beta2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#Thu Mar 02 18:32:01 PST 2017
|
#Sat Aug 12 15:52:56 PDT 2017
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-rc-1-all.zip
|
||||||