Android: Extend functionality of EglRenderer

The purpose is to prepare for a TextureViewRenderer that will need the
new functionality.

The new functionality is:
 * Be able to create an EglRenderer using a SurfaceTexture.
 * Fps reduction logic.
 * Log statistics every 4 seconds regardless of framerate.
 * Include swap buffer time in statistics.
 * Use EglBase10 if texture frames are disabled.
 * Function for printing stack trace of render thread.
 * Public clearImage() function for clearing the EGLSurface.

BUG=webrtc:6470

Review-Url: https://codereview.webrtc.org/2428933002
Cr-Commit-Position: refs/heads/master@{#14698}
This commit is contained in:
magjed
2016-10-20 03:18:09 -07:00
committed by Commit bot
parent ca20e7cfed
commit 9ab8a1884d

View File

@ -10,6 +10,7 @@
package org.webrtc; package org.webrtc;
import android.graphics.SurfaceTexture;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
@ -25,19 +26,26 @@ import java.util.concurrent.TimeUnit;
*/ */
public class EglRenderer implements VideoRenderer.Callbacks { public class EglRenderer implements VideoRenderer.Callbacks {
private static final String TAG = "EglRenderer"; private static final String TAG = "EglRenderer";
private static final long LOG_INTERVAL_SEC = 4;
private static final int MAX_SURFACE_CLEAR_COUNT = 3; private static final int MAX_SURFACE_CLEAR_COUNT = 3;
private class EglSurfaceCreation implements Runnable { private class EglSurfaceCreation implements Runnable {
private Surface surface; private Object surface;
public synchronized void setSurface(Surface surface) { public synchronized void setSurface(Object surface) {
this.surface = surface; this.surface = surface;
} }
@Override @Override
public synchronized void run() { public synchronized void run() {
if (surface != null && eglBase != null && !eglBase.hasSurface()) { if (surface != null && eglBase != null && !eglBase.hasSurface()) {
if (surface instanceof Surface) {
eglBase.createSurface((Surface) surface); eglBase.createSurface((Surface) surface);
} else if (surface instanceof SurfaceTexture) {
eglBase.createSurface((SurfaceTexture) surface);
} else {
throw new IllegalStateException("Invalid surface: " + surface);
}
eglBase.makeCurrent(); eglBase.makeCurrent();
// Necessary for YUV frames with odd width. // Necessary for YUV frames with odd width.
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
@ -52,6 +60,14 @@ public class EglRenderer implements VideoRenderer.Callbacks {
private final Object handlerLock = new Object(); private final Object handlerLock = new Object();
private Handler renderThreadHandler; private Handler renderThreadHandler;
// Variables for fps reduction.
private final Object fpsReductionLock = new Object();
// Time for when next frame should be rendered.
private long nextFrameTimeNs;
// Minimum duration between frames when fps reduction is active, or -1 if video is completely
// paused.
private long minRenderPeriodNs;
// EGL and GL resources for drawing YUV/OES textures. After initilization, these are only accessed // EGL and GL resources for drawing YUV/OES textures. After initilization, these are only accessed
// from the render thread. // from the render thread.
private EglBase eglBase; private EglBase eglBase;
@ -81,10 +97,12 @@ public class EglRenderer implements VideoRenderer.Callbacks {
private int framesDropped; private int framesDropped;
// Number of rendered video frames. // Number of rendered video frames.
private int framesRendered; private int framesRendered;
// Time in ns when the first video frame was rendered. // Start time for counting these statistics, or 0 if we haven't started measuring yet.
private long firstFrameTimeNs; private long statisticsStartTimeNs;
// Time in ns spent in renderFrameOnRenderThread() function. // Time in ns spent in renderFrameOnRenderThread() function.
private long renderTimeNs; private long renderTimeNs;
// Time in ns spent by the render thread in the swapBuffers() function.
private long renderSwapBufferTimeNs;
// Runnable for posting frames to render thread. // Runnable for posting frames to render thread.
private final Runnable renderFrameRunnable = new Runnable() { private final Runnable renderFrameRunnable = new Runnable() {
@ -94,6 +112,20 @@ public class EglRenderer implements VideoRenderer.Callbacks {
} }
}; };
private final Runnable logStatisticsRunnable = new Runnable() {
@Override
public void run() {
logStatistics();
synchronized (handlerLock) {
if (renderThreadHandler != null) {
renderThreadHandler.removeCallbacks(logStatisticsRunnable);
renderThreadHandler.postDelayed(
logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC));
}
}
}
};
private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation(); private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation();
/** /**
@ -128,15 +160,37 @@ public class EglRenderer implements VideoRenderer.Callbacks {
ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() { ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
@Override @Override
public void run() { public void run() {
// If sharedContext is null, then texture frames are disabled. This is typically for old
// devices that might not be fully spec compliant, so force EGL 1.0 since EGL 1.4 has
// caused trouble on some weird devices.
if (sharedContext == null) {
logD("EglBase10.create context");
eglBase = new EglBase10(null /* sharedContext */, configAttributes);
} else {
logD("EglBase.create shared context");
eglBase = EglBase.create(sharedContext, configAttributes); eglBase = EglBase.create(sharedContext, configAttributes);
} }
}
}); });
renderThreadHandler.post(eglSurfaceCreationRunnable);
final long currentTimeNs = System.nanoTime();
resetStatistics(currentTimeNs);
renderThreadHandler.postDelayed(
logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC));
} }
} }
public void createEglSurface(Surface surface) { public void createEglSurface(Surface surface) {
createEglSurfaceInternal(surface);
}
public void createEglSurface(SurfaceTexture surfaceTexture) {
createEglSurfaceInternal(surfaceTexture);
}
private void createEglSurfaceInternal(Object surface) {
eglSurfaceCreationRunnable.setSurface(surface); eglSurfaceCreationRunnable.setSurface(surface);
runOnRenderThread(eglSurfaceCreationRunnable); postToRenderThread(eglSurfaceCreationRunnable);
} }
/** /**
@ -146,12 +200,14 @@ public class EglRenderer implements VideoRenderer.Callbacks {
* don't call this function, the GL resources might leak. * don't call this function, the GL resources might leak.
*/ */
public void release() { public void release() {
logD("Releasing.");
final CountDownLatch eglCleanupBarrier = new CountDownLatch(1); final CountDownLatch eglCleanupBarrier = new CountDownLatch(1);
synchronized (handlerLock) { synchronized (handlerLock) {
if (renderThreadHandler == null) { if (renderThreadHandler == null) {
logD("Already released"); logD("Already released");
return; return;
} }
renderThreadHandler.removeCallbacks(logStatisticsRunnable);
// Release EGL and GL resources on render thread. // Release EGL and GL resources on render thread.
renderThreadHandler.postAtFrontOfQueue(new Runnable() { renderThreadHandler.postAtFrontOfQueue(new Runnable() {
@Override @Override
@ -193,21 +249,36 @@ public class EglRenderer implements VideoRenderer.Callbacks {
pendingFrame = null; pendingFrame = null;
} }
} }
resetStatistics();
logD("Releasing done."); logD("Releasing done.");
} }
/** /**
* Reset statistics. This will reset the logged statistics in logStatistics(), and * Reset the statistics logged in logStatistics().
* RendererEvents.onFirstFrameRendered() will be called for the next frame.
*/ */
public void resetStatistics() { private void resetStatistics(long currentTimeNs) {
synchronized (statisticsLock) { synchronized (statisticsLock) {
statisticsStartTimeNs = currentTimeNs;
framesReceived = 0; framesReceived = 0;
framesDropped = 0; framesDropped = 0;
framesRendered = 0; framesRendered = 0;
firstFrameTimeNs = 0;
renderTimeNs = 0; renderTimeNs = 0;
renderSwapBufferTimeNs = 0;
}
}
public void printStackTrace() {
synchronized (handlerLock) {
final Thread renderThread =
(renderThreadHandler == null) ? null : renderThreadHandler.getLooper().getThread();
if (renderThread != null) {
final StackTraceElement[] renderStackTrace = renderThread.getStackTrace();
if (renderStackTrace.length > 0) {
logD("EglRenderer stack trace:");
for (StackTraceElement traceElem : renderStackTrace) {
logD(traceElem.toString());
}
}
}
} }
} }
@ -232,30 +303,77 @@ public class EglRenderer implements VideoRenderer.Callbacks {
} }
} }
/**
* Limit render framerate.
*
* @param fps Limit render framerate to this value, or use Float.POSITIVE_INFINITY to disable fps
* reduction.
*/
public void setFpsReduction(float fps) {
logD("setFpsReduction: " + fps);
synchronized (fpsReductionLock) {
final long previousRenderPeriodNs = minRenderPeriodNs;
if (fps <= 0) {
minRenderPeriodNs = Long.MAX_VALUE;
} else {
minRenderPeriodNs = (long) (TimeUnit.SECONDS.toNanos(1) / fps);
}
if (minRenderPeriodNs != previousRenderPeriodNs) {
// Fps reduction changed - reset frame time.
nextFrameTimeNs = System.nanoTime();
}
}
}
public void disableFpsReduction() {
setFpsReduction(Float.POSITIVE_INFINITY /* fps */);
}
public void pauseVideo() {
setFpsReduction(0 /* fps */);
}
// VideoRenderer.Callbacks interface. // VideoRenderer.Callbacks interface.
@Override @Override
public void renderFrame(VideoRenderer.I420Frame frame) { public void renderFrame(VideoRenderer.I420Frame frame) {
synchronized (statisticsLock) { synchronized (statisticsLock) {
++framesReceived; ++framesReceived;
} }
final boolean dropOldFrame;
synchronized (handlerLock) { synchronized (handlerLock) {
if (renderThreadHandler == null) { if (renderThreadHandler == null) {
logD("Dropping frame - Not initialized or already released."); logD("Dropping frame - Not initialized or already released.");
VideoRenderer.renderFrameDone(frame); VideoRenderer.renderFrameDone(frame);
return; return;
} }
synchronized (frameLock) { // Check if fps reduction is active.
if (pendingFrame != null) { synchronized (fpsReductionLock) {
// Drop old frame. if (minRenderPeriodNs > 0) {
synchronized (statisticsLock) { final long currentTimeNs = System.nanoTime();
++framesDropped; if (currentTimeNs < nextFrameTimeNs) {
logD("Dropping frame - fps reduction is active.");
VideoRenderer.renderFrameDone(frame);
return;
} }
nextFrameTimeNs += minRenderPeriodNs;
// The time for the next frame should always be in the future.
nextFrameTimeNs = Math.max(nextFrameTimeNs, currentTimeNs);
}
}
synchronized (frameLock) {
dropOldFrame = (pendingFrame != null);
if (dropOldFrame) {
VideoRenderer.renderFrameDone(pendingFrame); VideoRenderer.renderFrameDone(pendingFrame);
} }
pendingFrame = frame; pendingFrame = frame;
renderThreadHandler.post(renderFrameRunnable); renderThreadHandler.post(renderFrameRunnable);
} }
} }
if (dropOldFrame) {
synchronized (statisticsLock) {
++framesDropped;
}
}
} }
/** /**
@ -295,7 +413,7 @@ public class EglRenderer implements VideoRenderer.Callbacks {
/** /**
* Private helper function to post tasks safely. * Private helper function to post tasks safely.
*/ */
private void runOnRenderThread(Runnable runnable) { private void postToRenderThread(Runnable runnable) {
synchronized (handlerLock) { synchronized (handlerLock) {
if (renderThreadHandler != null) { if (renderThreadHandler != null) {
renderThreadHandler.post(runnable); renderThreadHandler.post(runnable);
@ -303,7 +421,7 @@ public class EglRenderer implements VideoRenderer.Callbacks {
} }
} }
private void makeBlack() { private void clearSurfaceOnRenderThread() {
if (eglBase != null && eglBase.hasSurface()) { if (eglBase != null && eglBase.hasSurface()) {
logD("clearSurface"); logD("clearSurface");
GLES20.glClearColor(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */); GLES20.glClearColor(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */);
@ -312,6 +430,23 @@ public class EglRenderer implements VideoRenderer.Callbacks {
} }
} }
/**
* Post a task to clear the TextureView to a transparent uniform color.
*/
public void clearImage() {
synchronized (handlerLock) {
if (renderThreadHandler == null) {
return;
}
renderThreadHandler.postAtFrontOfQueue(new Runnable() {
@Override
public void run() {
clearSurfaceOnRenderThread();
}
});
}
}
/** /**
* Renders and releases |pendingFrame|. * Renders and releases |pendingFrame|.
*/ */
@ -348,7 +483,7 @@ public class EglRenderer implements VideoRenderer.Callbacks {
return; return;
} }
logD("Surface size mismatch - clearing surface."); logD("Surface size mismatch - clearing surface.");
makeBlack(); clearSurfaceOnRenderThread();
} }
final float[] layoutMatrix; final float[] layoutMatrix;
if (layoutAspectRatio > 0) { if (layoutAspectRatio > 0) {
@ -380,30 +515,39 @@ public class EglRenderer implements VideoRenderer.Callbacks {
surfaceWidth, surfaceHeight); surfaceWidth, surfaceHeight);
} }
final long swapBuffersStartTimeNs = System.nanoTime();
eglBase.swapBuffers(); eglBase.swapBuffers();
VideoRenderer.renderFrameDone(frame); VideoRenderer.renderFrameDone(frame);
final long currentTimeNs = System.nanoTime();
synchronized (statisticsLock) { synchronized (statisticsLock) {
if (framesRendered == 0) {
firstFrameTimeNs = startTimeNs;
}
++framesRendered; ++framesRendered;
renderTimeNs += (System.nanoTime() - startTimeNs); renderTimeNs += (currentTimeNs - startTimeNs);
if (framesRendered % 300 == 0) { renderSwapBufferTimeNs += (currentTimeNs - swapBuffersStartTimeNs);
logStatistics();
}
} }
} }
private void logStatistics() { private String averageTimeAsString(long sumTimeNs, int count) {
synchronized (statisticsLock) { return (count <= 0) ? "NA" : TimeUnit.NANOSECONDS.toMicros(sumTimeNs / count) + " μs";
logD("Frames received: " + framesReceived + ". Dropped: " + framesDropped + ". Rendered: "
+ framesRendered);
if (framesReceived > 0 && framesRendered > 0) {
final long timeSinceFirstFrameNs = System.nanoTime() - firstFrameTimeNs;
logD("Duration: " + (int) (timeSinceFirstFrameNs / 1e6) + " ms. FPS: "
+ framesRendered * 1e9 / timeSinceFirstFrameNs);
logD("Average render time: " + (int) (renderTimeNs / (1000 * framesRendered)) + " us.");
} }
private void logStatistics() {
final long currentTimeNs = System.nanoTime();
synchronized (statisticsLock) {
final long elapsedTimeNs = currentTimeNs - statisticsStartTimeNs;
if (elapsedTimeNs <= 0) {
return;
}
final float renderFps = framesRendered * TimeUnit.SECONDS.toNanos(1) / (float) elapsedTimeNs;
logD("Duration: " + TimeUnit.NANOSECONDS.toMillis(elapsedTimeNs) + " ms."
+ " Frames received: " + framesReceived + "."
+ " Dropped: " + framesDropped + "."
+ " Rendered: " + framesRendered + "."
+ " Render fps: " + String.format("%.1f", renderFps) + "."
+ " Average render time: " + averageTimeAsString(renderTimeNs, framesRendered) + "."
+ " Average swapBuffer time: "
+ averageTimeAsString(renderSwapBufferTimeNs, framesRendered) + ".");
resetStatistics(currentTimeNs);
} }
} }