| Index: webrtc/api/android/java/src/org/webrtc/EglRenderer.java
|
| diff --git a/webrtc/api/android/java/src/org/webrtc/EglRenderer.java b/webrtc/api/android/java/src/org/webrtc/EglRenderer.java
|
| index f5b9198c56030cc655ce2949d2d45f36297fceca..415e1278541b0c7fe0fd59e9f0d01085e2e59399 100644
|
| --- a/webrtc/api/android/java/src/org/webrtc/EglRenderer.java
|
| +++ b/webrtc/api/android/java/src/org/webrtc/EglRenderer.java
|
| @@ -10,6 +10,7 @@
|
|
|
| package org.webrtc;
|
|
|
| +import android.graphics.SurfaceTexture;
|
| import android.opengl.GLES20;
|
| import android.os.Handler;
|
| import android.os.HandlerThread;
|
| @@ -25,19 +26,26 @@ import java.util.concurrent.TimeUnit;
|
| */
|
| public class EglRenderer implements VideoRenderer.Callbacks {
|
| private static final String TAG = "EglRenderer";
|
| + private static final long LOG_INTERVAL_SEC = 4;
|
| private static final int MAX_SURFACE_CLEAR_COUNT = 3;
|
|
|
| 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;
|
| }
|
|
|
| @Override
|
| public synchronized void run() {
|
| if (surface != null && eglBase != null && !eglBase.hasSurface()) {
|
| - eglBase.createSurface((Surface) surface);
|
| + if (surface instanceof Surface) {
|
| + eglBase.createSurface((Surface) surface);
|
| + } else if (surface instanceof SurfaceTexture) {
|
| + eglBase.createSurface((SurfaceTexture) surface);
|
| + } else {
|
| + throw new IllegalStateException("Invalid surface: " + surface);
|
| + }
|
| eglBase.makeCurrent();
|
| // Necessary for YUV frames with odd width.
|
| GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
|
| @@ -52,6 +60,14 @@ public class EglRenderer implements VideoRenderer.Callbacks {
|
| private final Object handlerLock = new Object();
|
| 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
|
| // from the render thread.
|
| private EglBase eglBase;
|
| @@ -81,10 +97,12 @@ public class EglRenderer implements VideoRenderer.Callbacks {
|
| private int framesDropped;
|
| // Number of rendered video frames.
|
| private int framesRendered;
|
| - // Time in ns when the first video frame was rendered.
|
| - private long firstFrameTimeNs;
|
| + // Start time for counting these statistics, or 0 if we haven't started measuring yet.
|
| + private long statisticsStartTimeNs;
|
| // Time in ns spent in renderFrameOnRenderThread() function.
|
| 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.
|
| 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();
|
|
|
| /**
|
| @@ -128,15 +160,37 @@ public class EglRenderer implements VideoRenderer.Callbacks {
|
| ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
|
| @Override
|
| public void run() {
|
| - eglBase = EglBase.create(sharedContext, configAttributes);
|
| + // 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);
|
| + }
|
| }
|
| });
|
| + renderThreadHandler.post(eglSurfaceCreationRunnable);
|
| + final long currentTimeNs = System.nanoTime();
|
| + resetStatistics(currentTimeNs);
|
| + renderThreadHandler.postDelayed(
|
| + logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC));
|
| }
|
| }
|
|
|
| public void createEglSurface(Surface surface) {
|
| + createEglSurfaceInternal(surface);
|
| + }
|
| +
|
| + public void createEglSurface(SurfaceTexture surfaceTexture) {
|
| + createEglSurfaceInternal(surfaceTexture);
|
| + }
|
| +
|
| + private void createEglSurfaceInternal(Object 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.
|
| */
|
| public void release() {
|
| + logD("Releasing.");
|
| final CountDownLatch eglCleanupBarrier = new CountDownLatch(1);
|
| synchronized (handlerLock) {
|
| if (renderThreadHandler == null) {
|
| logD("Already released");
|
| return;
|
| }
|
| + renderThreadHandler.removeCallbacks(logStatisticsRunnable);
|
| // Release EGL and GL resources on render thread.
|
| renderThreadHandler.postAtFrontOfQueue(new Runnable() {
|
| @Override
|
| @@ -193,21 +249,36 @@ public class EglRenderer implements VideoRenderer.Callbacks {
|
| pendingFrame = null;
|
| }
|
| }
|
| - resetStatistics();
|
| logD("Releasing done.");
|
| }
|
|
|
| /**
|
| - * Reset statistics. This will reset the logged statistics in logStatistics(), and
|
| - * RendererEvents.onFirstFrameRendered() will be called for the next frame.
|
| + * Reset the statistics logged in logStatistics().
|
| */
|
| - public void resetStatistics() {
|
| + private void resetStatistics(long currentTimeNs) {
|
| synchronized (statisticsLock) {
|
| + statisticsStartTimeNs = currentTimeNs;
|
| framesReceived = 0;
|
| framesDropped = 0;
|
| framesRendered = 0;
|
| - firstFrameTimeNs = 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.
|
| @Override
|
| public void renderFrame(VideoRenderer.I420Frame frame) {
|
| synchronized (statisticsLock) {
|
| ++framesReceived;
|
| }
|
| + final boolean dropOldFrame;
|
| synchronized (handlerLock) {
|
| if (renderThreadHandler == null) {
|
| logD("Dropping frame - Not initialized or already released.");
|
| VideoRenderer.renderFrameDone(frame);
|
| return;
|
| }
|
| - synchronized (frameLock) {
|
| - if (pendingFrame != null) {
|
| - // Drop old frame.
|
| - synchronized (statisticsLock) {
|
| - ++framesDropped;
|
| + // Check if fps reduction is active.
|
| + synchronized (fpsReductionLock) {
|
| + if (minRenderPeriodNs > 0) {
|
| + final long currentTimeNs = System.nanoTime();
|
| + 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);
|
| }
|
| pendingFrame = frame;
|
| 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 void runOnRenderThread(Runnable runnable) {
|
| + private void postToRenderThread(Runnable runnable) {
|
| synchronized (handlerLock) {
|
| if (renderThreadHandler != null) {
|
| renderThreadHandler.post(runnable);
|
| @@ -303,7 +421,7 @@ public class EglRenderer implements VideoRenderer.Callbacks {
|
| }
|
| }
|
|
|
| - private void makeBlack() {
|
| + private void clearSurfaceOnRenderThread() {
|
| if (eglBase != null && eglBase.hasSurface()) {
|
| logD("clearSurface");
|
| GLES20.glClearColor(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */);
|
| @@ -313,6 +431,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|.
|
| */
|
| private void renderFrameOnRenderThread() {
|
| @@ -348,7 +483,7 @@ public class EglRenderer implements VideoRenderer.Callbacks {
|
| return;
|
| }
|
| logD("Surface size mismatch - clearing surface.");
|
| - makeBlack();
|
| + clearSurfaceOnRenderThread();
|
| }
|
| final float[] layoutMatrix;
|
| if (layoutAspectRatio > 0) {
|
| @@ -380,30 +515,39 @@ public class EglRenderer implements VideoRenderer.Callbacks {
|
| surfaceWidth, surfaceHeight);
|
| }
|
|
|
| + final long swapBuffersStartTimeNs = System.nanoTime();
|
| eglBase.swapBuffers();
|
| VideoRenderer.renderFrameDone(frame);
|
| +
|
| + final long currentTimeNs = System.nanoTime();
|
| synchronized (statisticsLock) {
|
| - if (framesRendered == 0) {
|
| - firstFrameTimeNs = startTimeNs;
|
| - }
|
| ++framesRendered;
|
| - renderTimeNs += (System.nanoTime() - startTimeNs);
|
| - if (framesRendered % 300 == 0) {
|
| - logStatistics();
|
| - }
|
| + renderTimeNs += (currentTimeNs - startTimeNs);
|
| + renderSwapBufferTimeNs += (currentTimeNs - swapBuffersStartTimeNs);
|
| }
|
| }
|
|
|
| + private String averageTimeAsString(long sumTimeNs, int count) {
|
| + return (count <= 0) ? "NA" : TimeUnit.NANOSECONDS.toMicros(sumTimeNs / count) + " μs";
|
| + }
|
| +
|
| private void logStatistics() {
|
| + final long currentTimeNs = System.nanoTime();
|
| synchronized (statisticsLock) {
|
| - 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.");
|
| + 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);
|
| }
|
| }
|
|
|
|
|