mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-13 20:14:18 +00:00
Compare commits
1
Commits
v5.1
..
frame-pacing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91261f9520 |
+6
-12
@@ -4,23 +4,19 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 26
|
||||
buildToolsVersion '26.0.0'
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion '25.0.3'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 26
|
||||
targetSdkVersion 25
|
||||
|
||||
versionName "5.1"
|
||||
versionCode = 128
|
||||
versionName "5.0.1"
|
||||
versionCode = 123
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -56,8 +52,6 @@ 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')
|
||||
|
||||
debugCompile project(path:':moonlight-common', configuration:'debug')
|
||||
releaseCompile project(path:':moonlight-common', configuration:'release')
|
||||
}
|
||||
|
||||
@@ -171,20 +171,6 @@ 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();
|
||||
|
||||
@@ -214,7 +200,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, prefConfig.bitrate);
|
||||
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat);
|
||||
|
||||
// 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()) {
|
||||
@@ -271,6 +257,8 @@ 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,
|
||||
@@ -289,20 +277,6 @@ 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();
|
||||
@@ -446,17 +420,6 @@ 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();
|
||||
|
||||
@@ -490,6 +453,14 @@ 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();
|
||||
}
|
||||
@@ -700,8 +671,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 ||
|
||||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE)
|
||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||
{
|
||||
// This case is for mice
|
||||
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
@@ -710,11 +680,6 @@ 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);
|
||||
@@ -928,6 +893,12 @@ 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
|
||||
@@ -937,13 +908,11 @@ 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();
|
||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
||||
}
|
||||
@@ -951,14 +920,11 @@ 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();
|
||||
|
||||
stopConnection();
|
||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
||||
getResources().getString(R.string.conn_terminated_msg), true);
|
||||
}
|
||||
@@ -1025,10 +991,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
// Let the decoder know immediately that the surface is gone
|
||||
decoderRenderer.prepareForStop();
|
||||
|
||||
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();
|
||||
|
||||
stopConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +63,6 @@ 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
|
||||
|
||||
+2
-4
@@ -10,11 +10,11 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
||||
public class AndroidCaptureProvider extends InputCaptureProvider {
|
||||
private ViewGroup rootViewGroup;
|
||||
private Context context;
|
||||
|
||||
public AndroidPointerIconCaptureProvider(Activity activity) {
|
||||
public AndroidCaptureProvider(Activity activity) {
|
||||
this.context = activity;
|
||||
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||
}
|
||||
@@ -33,13 +33,11 @@ public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
||||
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
super.enableCapture();
|
||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
super.disableCapture();
|
||||
setPointerIconOnAllViews(null);
|
||||
}
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,15 @@ 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) {
|
||||
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using Android O+ native mouse capture");
|
||||
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
||||
}
|
||||
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||
// 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()) {
|
||||
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||
return new ShieldCaptureProvider(activity);
|
||||
}
|
||||
@@ -21,11 +19,9 @@ public class InputCaptureManager {
|
||||
LimeLog.info("Using Evdev mouse capture");
|
||||
return new EvdevCaptureProvider(activity, rootListener);
|
||||
}
|
||||
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 if (AndroidCaptureProvider.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using Android N+ native mouse capture");
|
||||
return new AndroidCaptureProvider(activity);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Mouse capture not available");
|
||||
|
||||
@@ -3,25 +3,10 @@ package com.limelight.binding.input.capture;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
public abstract class InputCaptureProvider {
|
||||
protected boolean isCapturing;
|
||||
|
||||
public void enableCapture() {
|
||||
isCapturing = true;
|
||||
}
|
||||
public void disableCapture() {
|
||||
isCapturing = false;
|
||||
}
|
||||
|
||||
public abstract void enableCapture();
|
||||
public abstract void disableCapture();
|
||||
public void destroy() {}
|
||||
|
||||
public boolean isCapturingEnabled() {
|
||||
return isCapturing;
|
||||
}
|
||||
|
||||
public boolean isCapturingActive() {
|
||||
return isCapturing;
|
||||
}
|
||||
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
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,13 +63,11 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
|
||||
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
super.enableCapture();
|
||||
setCursorVisibility(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
super.disableCapture();
|
||||
setCursorVisibility(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -221,7 +221,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
||||
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
super.enableCapture();
|
||||
if (!started) {
|
||||
// Start the handler thread if it's our first time
|
||||
// capturing
|
||||
@@ -248,7 +247,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
super.disableCapture();
|
||||
// This may be called on the main thread
|
||||
runInNetworkSafeContextSynchronously(new Runnable() {
|
||||
@Override
|
||||
|
||||
@@ -23,7 +23,17 @@ import android.view.SurfaceHolder;
|
||||
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
|
||||
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
|
||||
private ByteBuffer[] legacyInputBuffers;
|
||||
@@ -31,10 +41,6 @@ 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;
|
||||
@@ -45,8 +51,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
private boolean refFrameInvalidationActive;
|
||||
private int initialWidth, initialHeight;
|
||||
private int videoFormat;
|
||||
private SurfaceHolder renderTarget;
|
||||
private volatile boolean stopping;
|
||||
private Object renderTarget;
|
||||
private boolean stopping;
|
||||
|
||||
private boolean needsBaselineSpsHack;
|
||||
private SeqParameterSet savedSps;
|
||||
@@ -55,11 +61,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
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;
|
||||
@@ -101,15 +102,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
return decoderInfo;
|
||||
}
|
||||
|
||||
public void setRenderTarget(SurfaceHolder renderTarget) {
|
||||
public void setRenderTarget(Object renderTarget) {
|
||||
this.renderTarget = renderTarget;
|
||||
}
|
||||
|
||||
public MediaCodecDecoderRenderer(int videoFormat, int bitrate) {
|
||||
public MediaCodecDecoderRenderer(int videoFormat) {
|
||||
//dumpDecoders();
|
||||
|
||||
this.bitrate = bitrate;
|
||||
|
||||
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
|
||||
|
||||
avcDecoder = findAvcDecoder();
|
||||
@@ -168,7 +167,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
this.initialWidth = width;
|
||||
this.initialHeight = height;
|
||||
this.videoFormat = format;
|
||||
this.refreshRate = redrawRate;
|
||||
|
||||
String mimeType;
|
||||
String selectedDecoderName;
|
||||
@@ -246,7 +244,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
try {
|
||||
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
|
||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)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) {
|
||||
@@ -279,12 +277,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags, boolean throwOnTransient) {
|
||||
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (e instanceof CodecException) {
|
||||
CodecException codecExc = (CodecException) e;
|
||||
|
||||
if (codecExc.isTransient() && !throwOnTransient) {
|
||||
if (codecExc.isTransient()) {
|
||||
// We'll let transient exceptions go
|
||||
LimeLog.warning(codecExc.getDiagnosticInfo());
|
||||
return;
|
||||
@@ -294,8 +292,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Only throw if the surface is still around and we're not stopping
|
||||
if (!stopping && renderTarget.getSurface().isValid()) {
|
||||
// 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);
|
||||
}
|
||||
@@ -328,7 +327,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
// Render the last buffer
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
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);
|
||||
}
|
||||
|
||||
// Add delta time to the totals (excluding probable outliers)
|
||||
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||
@@ -351,7 +360,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, 0, false);
|
||||
handleDecoderException(e, null, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,11 +400,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
startTime = MediaCodecHelper.getMonotonicMillis();
|
||||
|
||||
try {
|
||||
while (index < 0 && !stopping) {
|
||||
while (rendererThread.isAlive() && index < 0 && !stopping) {
|
||||
index = videoDecoder.dequeueInputBuffer(10000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, 0, true);
|
||||
handleDecoderException(e, null, 0);
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
}
|
||||
|
||||
@@ -418,37 +427,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
startSpinnerThreads();
|
||||
}
|
||||
|
||||
public void prepareForStop() {
|
||||
// Let the decoding code know to ignore codec exceptions now
|
||||
stopping = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
stopping = true;
|
||||
|
||||
// Halt the rendering thread
|
||||
rendererThread.interrupt();
|
||||
|
||||
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();
|
||||
@@ -466,13 +454,31 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
private boolean queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
|
||||
try {
|
||||
videoDecoder.queueInputBuffer(inputBufferIndex,
|
||||
offset, length,
|
||||
timestampUs, 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
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, codecFlags, true);
|
||||
}
|
||||
else {
|
||||
// Failed to queue
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -483,12 +489,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
ByteBuffer buf;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
buf = videoDecoder.getInputBuffer(inputBufferIndex);
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(e, null, 0, true);
|
||||
return null;
|
||||
}
|
||||
buf = videoDecoder.getInputBuffer(inputBufferIndex);
|
||||
}
|
||||
else {
|
||||
buf = legacyInputBuffers[inputBufferIndex];
|
||||
@@ -519,36 +520,25 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public int submitDecodeUnit(byte[] frameData, int frameLength, int frameNumber, long receiveTimeMs) {
|
||||
public int submitDecodeUnit(byte[] frameData, int frameLength) {
|
||||
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;
|
||||
|
||||
@@ -650,18 +640,6 @@ 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_OK;
|
||||
}
|
||||
|
||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||
if (buf == null) {
|
||||
// We're being torn down now
|
||||
return MoonBridge.DR_OK;
|
||||
}
|
||||
|
||||
// Write the annex B header
|
||||
buf.put(frameData, 0, 5);
|
||||
|
||||
@@ -684,17 +662,6 @@ 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_OK;
|
||||
}
|
||||
|
||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||
if (buf == null) {
|
||||
// We're being torn down now
|
||||
return MoonBridge.DR_OK;
|
||||
}
|
||||
|
||||
if (needsBaselineSpsHack) {
|
||||
LimeLog.info("Saw PPS; disabling SPS hack");
|
||||
@@ -706,66 +673,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
else if (frameData[4] == 0x40) {
|
||||
numVpsIn++;
|
||||
|
||||
// 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;
|
||||
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
||||
}
|
||||
else if (frameData[4] == 0x42) {
|
||||
numSpsIn++;
|
||||
|
||||
// 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;
|
||||
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
||||
}
|
||||
else if (frameData[4] == 0x44) {
|
||||
numPpsIn++;
|
||||
|
||||
inputBufferIndex = dequeueInputBuffer();
|
||||
if (inputBufferIndex < 0) {
|
||||
// We're being torn down now
|
||||
return MoonBridge.DR_OK;
|
||||
}
|
||||
|
||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||
if (buf == null) {
|
||||
// We're being torn down now
|
||||
return MoonBridge.DR_OK;
|
||||
}
|
||||
|
||||
// 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_OK;
|
||||
}
|
||||
|
||||
buf = getEmptyInputBuffer(inputBufferIndex);
|
||||
if (buf == null) {
|
||||
// We're being torn down now
|
||||
return MoonBridge.DR_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy data from our buffer list into the input buffer
|
||||
buf.put(frameData, 0, frameLength);
|
||||
|
||||
if (!queueInputBuffer(inputBufferIndex,
|
||||
0, buf.position(),
|
||||
0, frameLength,
|
||||
timestampUs, codecFlags)) {
|
||||
return MoonBridge.DR_NEED_IDR;
|
||||
}
|
||||
@@ -788,9 +711,6 @@ 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});
|
||||
@@ -880,11 +800,8 @@ 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";
|
||||
|
||||
@@ -900,6 +817,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
|
||||
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
|
||||
|
||||
str += "/proc/cpuinfo:\n";
|
||||
try {
|
||||
str += MediaCodecHelper.readCpuinfo();
|
||||
} catch (Exception e) {
|
||||
str += e.getMessage();
|
||||
}
|
||||
|
||||
str += "Full decoder dump:\n";
|
||||
try {
|
||||
str += MediaCodecHelper.dumpDecoders();
|
||||
|
||||
@@ -63,24 +63,15 @@ public class MediaCodecHelper {
|
||||
|
||||
static {
|
||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Software decoders that don't support H264 high profile
|
||||
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 {
|
||||
@@ -107,11 +98,6 @@ 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");
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.3.3'
|
||||
classpath 'com.android.tools.build:gradle:2.3.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
Submodule moonlight-common updated: 13bf4f8855...d7f9be6c27
Reference in New Issue
Block a user