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:
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user