Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(411)

Unified Diff: webrtc/api/android/java/src/org/webrtc/EglRenderer.java

Issue 2392373002: Android: Split out EGL rendering from SurfaceViewRenderer to separate class (Closed)
Patch Set: Created 4 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « webrtc/api/BUILD.gn ('k') | webrtc/api/android/java/src/org/webrtc/RendererCommon.java » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
new file mode 100644
index 0000000000000000000000000000000000000000..9d3e794876890c8e9bab22d801000db424bc2eb4
--- /dev/null
+++ b/webrtc/api/android/java/src/org/webrtc/EglRenderer.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc;
+
+import android.view.Surface;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.graphics.SurfaceTexture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import android.opengl.GLES20;
+
+/**
+ * Implements org.webrtc.VideoRenderer.Callbacks by displaying the video stream on an EGL Surface.
+ * This class is intended to be used as a helper class for rendering on SurfaceViews and
+ * TextureViews.
+ */
+public class EglRenderer implements VideoRenderer.Callbacks {
+ private static final String TAG = "EglRenderer";
+ private static final long LOG_INTERVAL_SEC = 4;
+ private static final long LOG_INTERVAL_MS = TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC);
+ private static final long LOG_INTERVAL_NS = TimeUnit.SECONDS.toNanos(LOG_INTERVAL_SEC);
+ private static final int MAX_SURFACE_CLEAR_COUNT = 3;
+
+ private class EglSurfaceCreation implements Runnable {
+ private Object surface;
+
+ public synchronized void setSurface(Object surface) {
+ this.surface = surface;
+ }
+
+ @Override
+ public synchronized void run() {
+ if (surface != null && eglBase != null && !eglBase.hasSurface()) {
+ 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);
+ }
+ }
+ }
+
+ private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation();
+
+ private final Runnable eglSurfaceDeletionRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (eglBase != null) {
+ eglBase.detachCurrent();
+ eglBase.releaseSurface();
+ }
+ }
+ };
+
+ private final Runnable renderFrameRunnable = new Runnable() {
+ @Override
+ public void run() {
+ renderFrameOnRenderThread();
+ }
+ };
+
+ private final Runnable logStatisticsRunnable = new Runnable() {
+ @Override
+ public void run() {
+ logStatistics(System.nanoTime());
+ if (renderThreadHandler != null) {
+ renderThreadHandler.removeCallbacks(logStatisticsRunnable);
+ renderThreadHandler.postDelayed(logStatisticsRunnable, LOG_INTERVAL_MS);
+ }
+ }
+ };
+
+ private final String name;
+
+ // Dedicated render thread.
+ private HandlerThread renderThread;
+ // |renderThreadHandler| is a handler for communicating with |renderThread|, and is synchronized
+ // on |handlerLock|.
+ private final Object handlerLock = new Object();
+ private Handler renderThreadHandler;
+
+ // EGL and GL resources for drawing YUV/OES textures. After initilization, these are only
+ // accessed from the render thread.
+ private EglBase eglBase;
+ private final RendererCommon.YuvUploader yuvUploader = new RendererCommon.YuvUploader();
+ private RendererCommon.GlDrawer drawer;
+ // Texture ids for YUV frames. Allocated on first arrival of a YUV frame.
+ private int[] yuvTextures = null;
+
+ // Pending frame to render. Serves as a queue with size 1. Synchronized on |frameLock|.
+ private final Object frameLock = new Object();
+ private VideoRenderer.I420Frame pendingFrame;
+
+ // These variables are synchronized on |layoutLock|.
+ private final Object layoutLock = new Object();
+ private int surfaceWidth;
+ private int surfaceHeight;
+ private float layoutAspectRatio;
+ // If true, mirrors the video stream horizontally.
+ private boolean mirror;
+
+ // These variables are synchronized on |statisticsLock|.
+ private final Object statisticsLock = new Object();
+ // Start time for counting these statistics, or 0 if we haven't started measuring yet.
+ private long statisticsStartTimeNs;
+ // Total number of video frames received in renderFrame() call.
+ private int framesReceived;
+ // Number of video frames dropped by renderFrame() because previous frame has not been rendered
+ // yet.
+ private int framesDropped;
+ // Number of rendered video frames.
+ private int framesRendered;
+ // 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;
+
+ /**
+ * Standard constructor. The name will be used for the render thread name and included when
+ * logging. In order to render something, you must first call init() and createEglSurface.
+ */
+ public EglRenderer(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Set if the video stream should be mirrored or not.
+ */
+ public void setMirror(final boolean mirror) {
+ logD("setMirror: " + mirror);
+ synchronized (layoutLock) {
+ this.mirror = mirror;
+ }
+ }
+
+ /**
+ * Set layout aspect ratio. This is used to crop frames when rendering to avoid stretched video.
+ * Set this to 0 to disable cropping.
+ */
+ public void setLayoutAspectRatio(float layoutAspectRatio) {
+ logD("setLayoutAspectRatio: " + layoutAspectRatio);
+ synchronized (layoutLock) {
+ this.layoutAspectRatio = layoutAspectRatio;
+ }
+ }
+
+ /**
+ * Notify that the surface size has changed.
+ */
+ public void surfaceSizeChanged(int surfaceWidth, int surfaceHeight) {
+ logD("surfaceSizeChanged: " + surfaceWidth + "x" + surfaceHeight);
+ synchronized (layoutLock) {
+ this.surfaceWidth = surfaceWidth;
+ this.surfaceHeight = surfaceHeight;
+ }
+ }
+
+ /**
+ * Initialize this class, sharing resources with |sharedContext|. The custom |drawer| will be
+ * used for drawing frames on the EGLSurface. This class is responsible for calling release() on
+ * |drawer|. It is allowed to call init() to reinitialize the renderer after a previous
+ * init()/release() cycle.
+ */
+ public void init(final EglBase.Context sharedContext, final int[] configAttributes,
+ RendererCommon.GlDrawer drawer) {
+ ThreadUtils.checkIsOnMainThread();
+ resetStatistics(0 /* currentTimeNs */);
+ synchronized (handlerLock) {
+ if (renderThreadHandler != null) {
+ throw new IllegalStateException("Already initialized");
+ }
+ logD("Initializing EglRenderer");
+ this.drawer = drawer;
+ renderThread = new HandlerThread(name + "EglRenderer");
+ renderThread.start();
+ renderThreadHandler = new Handler(renderThread.getLooper());
+ // Create EGL context on the newly created render thread. It should be possibly to create
+ // the context on this thread and make it current on the render thread, but this causes
+ // failure on some Marvel based JB devices.
+ // https://bugs.chromium.org/p/webrtc/issues/detail?id=6350.
+ ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
+ @Override
+ public void run() {
+ if (sharedContext == null) {
+ logD("EglBase10.create context");
+ eglBase = new EglBase10(null /* sharedContext */, configAttributes);
+ } else {
+ logD("EglBase.create shared context");
+ eglBase = EglBase.create(sharedContext, configAttributes);
+ }
+ }
+ });
+ postToRenderThread(eglSurfaceCreationRunnable);
+ }
+ }
+
+ public void createEglSurface(Surface surface) {
+ createEglSurfaceInternal(surface);
+ }
+
+ public void createEglSurface(SurfaceTexture surfaceTexture) {
+ createEglSurfaceInternal(surfaceTexture);
+ }
+
+ /**
+ * Release EGL surface. This function will block until the EGL surface is released.
+ */
+ public void releaseEglSurface() {
+ // Ensure that the render thread is no longer touching the Surface before returning from this
+ // function.
+ eglSurfaceCreationRunnable.setSurface(null /* surface */);
+ synchronized (handlerLock) {
+ if (renderThreadHandler != null) {
+ renderThreadHandler.removeCallbacks(eglSurfaceCreationRunnable);
+ ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, eglSurfaceDeletionRunnable);
+ }
+ }
+ }
+
+ /**
+ * Clear the EGL Surface to a transparent/black uniform color.
+ */
+ public void clearSurface() {
+ synchronized (handlerLock) {
+ if (renderThreadHandler == null) {
+ return;
+ }
+ renderThreadHandler.postAtFrontOfQueue(new Runnable() {
+ @Override
+ public void run() {
+ clearSurfaceOnRenderThread();
+ }
+ });
+ }
+ }
+
+ // VideoRenderer.Callbacks interface.
+ @Override
+ public void renderFrame(VideoRenderer.I420Frame frame) {
+ synchronized (statisticsLock) {
+ ++framesReceived;
+ }
+ 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;
+ }
+ VideoRenderer.renderFrameDone(pendingFrame);
+ }
+ pendingFrame = frame;
+ renderThreadHandler.post(renderFrameRunnable);
+ }
+ }
+ }
+
+ /**
+ * Block until any pending frame is returned and all GL resources released, even if an interrupt
+ * occurs. If an interrupt occurs during release(), the interrupt flag will be set. This
+ * function should be called before the Activity is destroyed and the EGLContext is still valid.
+ * If you don't call this function, the GL resources might leak.
+ */
+ public void release() {
+ ThreadUtils.checkIsOnMainThread();
+ 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
+ public void run() {
+ if (drawer != null) {
+ drawer.release();
+ drawer = null;
+ }
+ if (yuvTextures != null) {
+ GLES20.glDeleteTextures(3, yuvTextures, 0);
+ yuvTextures = null;
+ }
+ if (eglBase != null) {
+ logD("eglBase detach and release.");
+ eglBase.detachCurrent();
+ eglBase.release();
+ eglBase = null;
+ }
+ eglCleanupBarrier.countDown();
+ }
+ });
+ final Looper renderLooper = renderThreadHandler.getLooper();
+ // Replace this post() with renderLooper.quitSafely() when API support >= 18.
+ renderThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ logD("Quitting render thread.");
+ renderLooper.quit();
+ }
+ });
+ // Don't accept any more frames or messages to the render thread.
+ renderThreadHandler = null;
+ }
+ // Make sure the EGL/GL cleanup posted above is executed.
+ ThreadUtils.awaitUninterruptibly(eglCleanupBarrier);
+ synchronized (frameLock) {
+ if (pendingFrame != null) {
+ VideoRenderer.renderFrameDone(pendingFrame);
+ pendingFrame = null;
+ }
+ }
+ logD("Releasing done.");
+ }
+
+ 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());
+ }
+ }
+ }
+ }
+ }
+
+ private void clearSurfaceOnRenderThread() {
+ if (eglBase != null && eglBase.hasSurface()) {
+ logD("clearSurface");
+ GLES20.glClearColor(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ eglBase.swapBuffers();
+ }
+ }
+
+ /**
+ * Private helper function to post tasks safely.
+ */
+ private void postToRenderThread(Runnable runnable) {
+ synchronized (handlerLock) {
+ if (renderThreadHandler != null) {
+ renderThreadHandler.post(runnable);
+ }
+ }
+ }
+
+ private void createEglSurfaceInternal(Object surface) {
+ eglSurfaceCreationRunnable.setSurface(surface);
+ postToRenderThread(eglSurfaceCreationRunnable);
+ }
+
+ /**
+ * Renders and releases |pendingFrame|.
+ */
+ private void renderFrameOnRenderThread() {
+ // Fetch and render |pendingFrame|.
+ final VideoRenderer.I420Frame frame;
+ synchronized (frameLock) {
+ if (pendingFrame == null) {
+ return;
+ }
+ frame = pendingFrame;
+ pendingFrame = null;
+ }
+ if (eglBase == null || !eglBase.hasSurface()) {
+ logE("Dropping frame - No surface");
+ VideoRenderer.renderFrameDone(frame);
+ return;
+ }
+
+ final long startTimeNs = System.nanoTime();
+ float[] texMatrix =
+ RendererCommon.rotateTextureMatrix(frame.samplingMatrix, frame.rotationDegree);
+
+ // After a surface size change, the EGLSurface might still have a buffer of the old size in
+ // the pipeline. Querying the EGLSurface will show if the underlying buffer dimensions haven't
+ // yet changed. Such a buffer will be rendered incorrectly, so flush it with a black frame.
+ synchronized (layoutLock) {
+ int surfaceClearCount = 0;
+ while (eglBase.surfaceWidth() != surfaceWidth || eglBase.surfaceHeight() != surfaceHeight) {
+ ++surfaceClearCount;
+ if (surfaceClearCount > MAX_SURFACE_CLEAR_COUNT) {
+ logD("Failed to get surface of expected size - dropping frame.");
+ VideoRenderer.renderFrameDone(frame);
+ return;
+ }
+ logD("Surface size mismatch - clearing surface.");
+ clearSurfaceOnRenderThread();
+ }
+ final float[] layoutMatrix;
+ if (layoutAspectRatio > 0) {
+ layoutMatrix = RendererCommon.getLayoutMatrix(
+ mirror, frame.rotatedWidth() / (float) frame.rotatedHeight(), layoutAspectRatio);
+ } else {
+ layoutMatrix =
+ mirror ? RendererCommon.horizontalFlipMatrix() : RendererCommon.identityMatrix();
+ }
+ texMatrix = RendererCommon.multiplyMatrices(texMatrix, layoutMatrix);
+ }
+
+ GLES20.glClearColor(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ if (frame.yuvFrame) {
+ // Make sure YUV textures are allocated.
+ if (yuvTextures == null) {
+ yuvTextures = new int[3];
+ for (int i = 0; i < 3; i++) {
+ yuvTextures[i] = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D);
+ }
+ }
+ yuvUploader.uploadYuvData(
+ yuvTextures, frame.width, frame.height, frame.yuvStrides, frame.yuvPlanes);
+ drawer.drawYuv(yuvTextures, texMatrix, frame.rotatedWidth(), frame.rotatedHeight(), 0, 0,
+ surfaceWidth, surfaceHeight);
+ } else {
+ drawer.drawOes(frame.textureId, texMatrix, frame.rotatedWidth(), frame.rotatedHeight(), 0, 0,
+ surfaceWidth, surfaceHeight);
+ }
+
+ final long swapBuffersStartTimeNs = System.nanoTime();
+ eglBase.swapBuffers();
+ VideoRenderer.renderFrameDone(frame);
+
+ final long currentTimeNs = System.nanoTime();
+ synchronized (statisticsLock) {
+ if (statisticsStartTimeNs == 0) {
+ // First frame rendered - start measuring statistics.
+ resetStatistics(currentTimeNs);
+ } else {
+ ++framesRendered;
+ renderTimeNs += (currentTimeNs - startTimeNs);
+ renderSwapBufferTimeNs += (currentTimeNs - swapBuffersStartTimeNs);
+ logStatistics(currentTimeNs);
+ }
+ }
+ }
+
+ /**
+ * Reset the statistics logged in logStatistics().
+ */
+ private void resetStatistics(long currentTimeNs) {
+ synchronized (statisticsLock) {
+ statisticsStartTimeNs = currentTimeNs;
+ framesReceived = 0;
+ framesDropped = 0;
+ framesRendered = 0;
+ renderTimeNs = 0;
+ renderSwapBufferTimeNs = 0;
+ }
+ }
+
+ private void logStatistics(long currentTimeNs) {
+ synchronized (statisticsLock) {
+ final long elapsedTimeNs = currentTimeNs - statisticsStartTimeNs;
+ if (elapsedTimeNs > LOG_INTERVAL_NS) {
+ logD("Frames received: " + framesReceived + ". Dropped: " + framesDropped + ". Rendered: "
+ + framesRendered);
+ if (framesReceived > 0 && framesRendered > 0 && elapsedTimeNs > 0) {
+ final long fps =
+ (framesRendered * TimeUnit.SECONDS.toNanos(1) + elapsedTimeNs / 2) / elapsedTimeNs;
+ logD("Duration: " + TimeUnit.NANOSECONDS.toMillis(elapsedTimeNs) + " ms. "
+ + "FPS: " + fps);
+ logD("Average render time: "
+ + TimeUnit.NANOSECONDS.toMicros(renderTimeNs / framesRendered) + " us, "
+ + "Average total swapBuffer time: "
+ + TimeUnit.NANOSECONDS.toMicros(renderSwapBufferTimeNs / framesRendered) + " us.");
+ }
+ resetStatistics(currentTimeNs);
+ }
+ }
+ }
+
+ private void logD(String string) {
+ Logging.d(TAG, name + string);
+ }
+
+ private void logE(String string) {
+ Logging.e(TAG, name + string);
+ }
+
+ private void logE(String string, Exception e) {
+ Logging.e(TAG, name + string, e);
+ }
+}
« no previous file with comments | « webrtc/api/BUILD.gn ('k') | webrtc/api/android/java/src/org/webrtc/RendererCommon.java » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698