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}
diff --git a/webrtc/api/android/java/src/org/webrtc/EglRenderer.java b/webrtc/api/android/java/src/org/webrtc/EglRenderer.java
index f5b9198..415e127 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 @@
  */
 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 @@
   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 @@
   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 @@
     }
   };
 
+  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 @@
       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 @@
    * 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 @@
         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 @@
     }
   }
 
+  /**
+   * 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 @@
   /**
    * 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 @@
     }
   }
 
-  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 @@
   }
 
   /**
+   * 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 @@
           return;
         }
         logD("Surface size mismatch - clearing surface.");
-        makeBlack();
+        clearSurfaceOnRenderThread();
       }
       final float[] layoutMatrix;
       if (layoutAspectRatio > 0) {
@@ -380,30 +515,39 @@
           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);
     }
   }