From 252285e4f7251f2a5ce55c47c31059a330e260f6 Mon Sep 17 00:00:00 2001 From: Kaiwen Xu Date: Sun, 7 Jul 2019 15:06:20 -0700 Subject: [PATCH 1/2] Implement performance overlay. --- app/src/main/java/com/limelight/Game.java | 30 +++++- .../video/MediaCodecDecoderRenderer.java | 102 +++++++++++++----- .../binding/video/PerfOverlayListener.java | 5 + .../limelight/binding/video/VideoStats.java | 70 ++++++++++++ .../preferences/PreferenceConfiguration.java | 4 + app/src/main/res/layout/activity_game.xml | 11 ++ app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/preferences.xml | 5 + 9 files changed, 202 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/limelight/binding/video/PerfOverlayListener.java create mode 100644 app/src/main/java/com/limelight/binding/video/VideoStats.java diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index c83e71d7..c888ff61 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -13,6 +13,7 @@ import com.limelight.binding.input.virtual_controller.VirtualController; import com.limelight.binding.video.CrashListener; import com.limelight.binding.video.MediaCodecDecoderRenderer; import com.limelight.binding.video.MediaCodecHelper; +import com.limelight.binding.video.PerfOverlayListener; import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.NvConnectionListener; import com.limelight.nvstream.StreamConfiguration; @@ -78,7 +79,8 @@ import java.util.Locale; public class Game extends Activity implements SurfaceHolder.Callback, OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener, - OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks + OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks, + PerfOverlayListener { private int lastMouseX = Integer.MIN_VALUE; private int lastMouseY = Integer.MIN_VALUE; @@ -113,6 +115,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, private boolean grabComboDown = false; private StreamView streamView; private TextView notificationOverlayView; + private TextView performanceOverlayView; private ShortcutHelper shortcutHelper; @@ -207,6 +210,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, notificationOverlayView = findViewById(R.id.notificationOverlay); + performanceOverlayView = findViewById(R.id.performanceOverlay); + inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -310,7 +315,14 @@ public class Game extends Activity implements SurfaceHolder.Callback, willStreamHdr = false; } - decoderRenderer = new MediaCodecDecoderRenderer(prefConfig, + // Check if the user has enabled performance stats overlay + if (prefConfig.enablePerfOverlay) { + performanceOverlayView.setVisibility(View.VISIBLE); + } + + decoderRenderer = new MediaCodecDecoderRenderer( + this, + prefConfig, new CrashListener() { @Override public void notifyCrash(Exception e) { @@ -325,8 +337,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, tombstonePrefs.getInt("CrashCount", 0), connMgr.isActiveNetworkMetered(), willStreamHdr, - glPrefs.glRenderer - ); + glPrefs.glRenderer, + this); // Don't stream HDR if the decoder can't support it if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) { @@ -1574,4 +1586,14 @@ public class Game extends Activity implements SurfaceHolder.Callback, hideSystemUi(2000); } } + + @Override + public void onPerfUpdate(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + performanceOverlayView.setText(text); + } + }); + } } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 7cc40a83..e46f15e7 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -8,10 +8,12 @@ import org.jcodec.codecs.h264.io.model.SeqParameterSet; import org.jcodec.codecs.h264.io.model.VUIParameters; import com.limelight.LimeLog; +import com.limelight.R; import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.jni.MoonBridge; import com.limelight.preferences.PreferenceConfiguration; +import android.content.Context; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; @@ -38,6 +40,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { private boolean submittedCsd; private boolean submitCsdNextCall; + private Context context; private MediaCodec videoDecoder; private Thread rendererThread; private boolean needsSpsBitstreamFixup, isExynos4; @@ -55,6 +58,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { private String glRenderer; private boolean foreground = true; private boolean legacyFrameDropRendering = false; + private PerfOverlayListener perfListener; private boolean needsBaselineSpsHack; private SeqParameterSet savedSps; @@ -63,13 +67,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { private long initialExceptionTimestamp; private static final int EXCEPTION_REPORT_DELAY_MS = 3000; + private VideoStats activeWindowVideoStats; + private VideoStats lastWindowVideoStats; + private VideoStats globalVideoStats; + private long lastTimestampUs; - private long decoderTimeMs; - private long totalTimeMs; - private int totalFramesReceived; - private int totalFramesRendered; - private int frameLossEvents; - private int framesLost; private int lastFrameNumber; private int refreshRate; private PreferenceConfiguration prefs; @@ -119,16 +121,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { this.renderTarget = renderTarget; } - public MediaCodecDecoderRenderer(PreferenceConfiguration prefs, + public MediaCodecDecoderRenderer(Context context, PreferenceConfiguration prefs, CrashListener crashListener, int consecutiveCrashCount, boolean meteredData, boolean requestedHdr, - String glRenderer) { + String glRenderer, PerfOverlayListener perfListener) { //dumpDecoders(); + this.context = context; this.prefs = prefs; this.crashListener = crashListener; this.consecutiveCrashCount = consecutiveCrashCount; this.glRenderer = glRenderer; + this.perfListener = perfListener; + + this.activeWindowVideoStats = new VideoStats(); + this.lastWindowVideoStats = new VideoStats(); + this.globalVideoStats = new VideoStats(); avcDecoder = findAvcDecoder(); if (avcDecoder != null) { @@ -311,7 +319,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000); if (delta >= 0 && delta < 1000) { if (USE_FRAME_RENDER_TIME) { - totalTimeMs += delta; + activeWindowVideoStats.totalTimeMs += delta; } } } @@ -421,14 +429,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { videoDecoder.releaseOutputBuffer(lastIndex, true); } - totalFramesRendered++; + activeWindowVideoStats.totalFramesRendered++; // Add delta time to the totals (excluding probable outliers) long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000); if (delta >= 0 && delta < 1000) { - decoderTimeMs += delta; + activeWindowVideoStats.decoderTimeMs += delta; if (!USE_FRAME_RENDER_TIME) { - totalTimeMs += delta; + activeWindowVideoStats.totalTimeMs += delta; } } } else { @@ -585,17 +593,57 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { return MoonBridge.DR_OK; } - totalFramesReceived++; - - // 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++; + if (lastFrameNumber == 0) { + activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis(); + } else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) { + // We can receive the same "frame" multiple times if it's an IDR frame. + // In that case, each frame start NALU is submitted independently. + activeWindowVideoStats.framesLost += frameNumber - lastFrameNumber - 1; + activeWindowVideoStats.totalFrames += frameNumber - lastFrameNumber - 1; + activeWindowVideoStats.frameLossEvents++; } lastFrameNumber = frameNumber; + // Flip stats windows roughly every second + if (System.currentTimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) { + if (prefs.enablePerfOverlay) { + VideoStats lastTwo = new VideoStats(); + lastTwo.add(lastWindowVideoStats); + lastTwo.add(activeWindowVideoStats); + VideoStatsFps fps = lastTwo.getFps(); + String decoder; + + if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) { + decoder = avcDecoder.getName(); + } else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) { + decoder = hevcDecoder.getName(); + } else { + decoder = "(unknown)"; + } + + String perfText = context.getString( + R.string.perf_overlay_text, + initialWidth + "x" + initialHeight, + decoder, + fps.totalFps, + fps.receivedFps, + fps.renderedFps, + (float)lastTwo.framesLost / lastTwo.totalFrames * 100, + (float)lastTwo.totalTimeMs / lastTwo.totalFramesReceived, + (float)lastTwo.decoderTimeMs / lastTwo.totalFramesReceived); + perfListener.onPerfUpdate(perfText); + } + + globalVideoStats.add(activeWindowVideoStats); + lastWindowVideoStats.copy(activeWindowVideoStats); + activeWindowVideoStats.clear(); + activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis(); + } + + activeWindowVideoStats.totalFramesReceived++; + activeWindowVideoStats.totalFrames++; + int inputBufferIndex; ByteBuffer buf; @@ -603,7 +651,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { if (!FRAME_RENDER_TIME_ONLY) { // Count time from first packet received to decode start - totalTimeMs += (timestampUs / 1000) - receiveTimeMs; + activeWindowVideoStats.totalTimeMs += (timestampUs / 1000) - receiveTimeMs; } if (timestampUs <= lastTimestampUs) { @@ -910,17 +958,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { } public int getAverageEndToEndLatency() { - if (totalFramesReceived == 0) { + if (globalVideoStats.totalFramesReceived == 0) { return 0; } - return (int)(totalTimeMs / totalFramesReceived); + return (int)(globalVideoStats.totalTimeMs / globalVideoStats.totalFramesReceived); } public int getAverageDecoderLatency() { - if (totalFramesReceived == 0) { + if (globalVideoStats.totalFramesReceived == 0) { return 0; } - return (int)(decoderTimeMs / totalFramesReceived); + return (int)(globalVideoStats.decoderTimeMs / globalVideoStats.totalFramesReceived); } static class DecoderHungException extends RuntimeException { @@ -981,9 +1029,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { str += "FPS target: "+renderer.refreshRate+"\n"; str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n"; str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n"; - str += "Total frames received: "+renderer.totalFramesReceived+"\n"; - str += "Total frames rendered: "+renderer.totalFramesRendered+"\n"; - str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\n"; + str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n"; + str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n"; + str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n"; str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n"; str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n"; diff --git a/app/src/main/java/com/limelight/binding/video/PerfOverlayListener.java b/app/src/main/java/com/limelight/binding/video/PerfOverlayListener.java new file mode 100644 index 00000000..281f95a0 --- /dev/null +++ b/app/src/main/java/com/limelight/binding/video/PerfOverlayListener.java @@ -0,0 +1,5 @@ +package com.limelight.binding.video; + +public interface PerfOverlayListener { + void onPerfUpdate(final String text); +} diff --git a/app/src/main/java/com/limelight/binding/video/VideoStats.java b/app/src/main/java/com/limelight/binding/video/VideoStats.java new file mode 100644 index 00000000..317714bf --- /dev/null +++ b/app/src/main/java/com/limelight/binding/video/VideoStats.java @@ -0,0 +1,70 @@ +package com.limelight.binding.video; + +class VideoStats { + + long decoderTimeMs; + long totalTimeMs; + int totalFrames; + int totalFramesReceived; + int totalFramesRendered; + int frameLossEvents; + int framesLost; + long measurementStartTimestamp; + + void add(VideoStats other) { + this.decoderTimeMs += other.decoderTimeMs; + this.totalTimeMs += other.totalTimeMs; + this.totalFrames += other.totalFrames; + this.totalFramesReceived += other.totalFramesReceived; + this.totalFramesRendered += other.totalFramesRendered; + this.frameLossEvents += other.frameLossEvents; + this.framesLost += other.framesLost; + + if (this.measurementStartTimestamp == 0) { + this.measurementStartTimestamp = other.measurementStartTimestamp; + } + + assert other.measurementStartTimestamp <= this.measurementStartTimestamp; + } + + void copy(VideoStats other) { + this.decoderTimeMs = other.decoderTimeMs; + this.totalTimeMs = other.totalTimeMs; + this.totalFrames = other.totalFrames; + this.totalFramesReceived = other.totalFramesReceived; + this.totalFramesRendered = other.totalFramesRendered; + this.frameLossEvents = other.frameLossEvents; + this.framesLost = other.framesLost; + this.measurementStartTimestamp = other.measurementStartTimestamp; + } + + void clear() { + this.decoderTimeMs = 0; + this.totalTimeMs = 0; + this.totalFrames = 0; + this.totalFramesReceived = 0; + this.totalFramesRendered = 0; + this.frameLossEvents = 0; + this.framesLost = 0; + this.measurementStartTimestamp = 0; + } + + VideoStatsFps getFps() { + float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000; + + VideoStatsFps fps = new VideoStatsFps(); + if (elapsed > 0) { + fps.totalFps = this.totalFrames / elapsed; + fps.receivedFps = this.totalFramesReceived / elapsed; + fps.renderedFps = this.totalFramesRendered / elapsed; + } + return fps; + } +} + +class VideoStatsFps { + + float totalFps; + float receivedFps; + float renderedFps; +} \ No newline at end of file diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index d4a65b9a..36f93b80 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -31,6 +31,7 @@ public class PreferenceConfiguration { private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop"; private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr"; private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip"; + private static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay"; private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all"; private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation"; private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons"; @@ -56,6 +57,7 @@ public class PreferenceConfiguration { private static final boolean DEFAULT_DISABLE_FRAME_DROP = false; private static final boolean DEFAULT_ENABLE_HDR = false; private static final boolean DEFAULT_ENABLE_PIP = false; + private static final boolean DEFAULT_ENABLE_PERF_OVERLAY = false; private static final boolean DEFAULT_BIND_ALL_USB = false; private static final boolean DEFAULT_MOUSE_EMULATION = true; private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false; @@ -79,6 +81,7 @@ public class PreferenceConfiguration { public boolean disableFrameDrop; public boolean enableHdr; public boolean enablePip; + public boolean enablePerfOverlay; public boolean bindAllUsb; public boolean mouseEmulation; public boolean mouseNavButtons; @@ -331,6 +334,7 @@ public class PreferenceConfiguration { config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP); config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR); config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP); + config.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY); config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB); config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION); config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS); diff --git a/app/src/main/res/layout/activity_game.xml b/app/src/main/res/layout/activity_game.xml index 5b6bd127..4a67cd70 100644 --- a/app/src/main/res/layout/activity_game.xml +++ b/app/src/main/res/layout/activity_game.xml @@ -10,6 +10,17 @@ android:layout_height="match_parent" android:layout_gravity="center" /> + + 16dp 16dp + + 8sp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 677b025a..96413b72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,6 +86,7 @@ Are you sure you want to delete this PC? Slow connection to PC\nReduce your bitrate Poor connection to PC + Video dimensions: %1$s\nDecoder: %2$s\nEstimated host PC frame rate: %3$.2f FPS\nIncoming frame rate from network: %4$.2f FPS\nRendering frame rate: %5$.2f FPS\nFrames dropped by your network connection: %6$.2f%%\nAverage frame time: %7$.2f ms\nAverage decoding time: %8$.2f ms Connecting to PC… @@ -185,5 +186,7 @@ H.265 lowers video bandwidth requirements but requires a very recent device Enable HDR (Experimental) Stream HDR when the game and PC GPU support it. HDR requires a GTX 1000 series GPU or later. + Enable performance overlay + Display performance stats overlay diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index db39edbf..fbdbe7cc 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -171,5 +171,10 @@ android:title="@string/title_enable_hdr" android:summary="@string/summary_enable_hdr" android:defaultValue="false" /> + From 01eb7a2b64a86a89281c412d09d6336d594970c6 Mon Sep 17 00:00:00 2001 From: Kaiwen Xu Date: Mon, 8 Jul 2019 01:12:36 -0700 Subject: [PATCH 2/2] Add executable permission to gradlew scripts. --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755