diff --git a/sdk/android/api/org/webrtc/EglRenderer.java b/sdk/android/api/org/webrtc/EglRenderer.java index a8dee89604..5ab0868ef3 100644 --- a/sdk/android/api/org/webrtc/EglRenderer.java +++ b/sdk/android/api/org/webrtc/EglRenderer.java @@ -14,8 +14,11 @@ import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.SurfaceTexture; import android.opengl.GLES20; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; import android.view.Surface; -import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import java.nio.ByteBuffer; import java.text.DecimalFormat; @@ -83,11 +86,35 @@ public class EglRenderer implements VideoSink { } } + /** + * Handler that triggers a callback when an uncaught exception happens when handling a message. + */ + private static class HandlerWithExceptionCallback extends Handler { + private final Runnable exceptionCallback; + + public HandlerWithExceptionCallback(Looper looper, Runnable exceptionCallback) { + super(looper); + this.exceptionCallback = exceptionCallback; + } + + @Override + public void dispatchMessage(Message msg) { + try { + super.dispatchMessage(msg); + } catch (Exception e) { + Logging.e(TAG, "Exception on EglRenderer thread", e); + exceptionCallback.run(); + throw e; + } + } + } + protected final String name; - // `eglThread` is used for rendering, and is synchronized on `threadLock`. - private final Object threadLock = new Object(); - @GuardedBy("threadLock") @Nullable private EglThread eglThread; + // `renderThreadHandler` is a handler for communicating with `renderThread`, and is synchronized + // on `handlerLock`. + private final Object handlerLock = new Object(); + @Nullable private Handler renderThreadHandler; private final ArrayList frameListeners = new ArrayList<>(); @@ -145,10 +172,10 @@ public class EglRenderer implements VideoSink { @Override public void run() { logStatistics(); - synchronized (threadLock) { - if (eglThread != null) { - eglThread.getHandler().removeCallbacks(logStatisticsRunnable); - eglThread.getHandler().postDelayed( + synchronized (handlerLock) { + if (renderThreadHandler != null) { + renderThreadHandler.removeCallbacks(logStatisticsRunnable); + renderThreadHandler.postDelayed( logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC)); } } @@ -158,8 +185,8 @@ public class EglRenderer implements VideoSink { private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation(); /** - * Standard constructor. The name will be included when logging. In order to render something, you - * must first call init() and createEglSurface. + * 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, new VideoFrameDrawer()); @@ -170,29 +197,6 @@ public class EglRenderer implements VideoSink { this.frameDrawer = videoFrameDrawer; } - public void init( - EglThread eglThread, RendererCommon.GlDrawer drawer, boolean usePresentationTimeStamp) { - synchronized (threadLock) { - if (this.eglThread != null) { - throw new IllegalStateException(name + "Already initialized"); - } - - logD("Initializing EglRenderer"); - this.eglThread = eglThread; - this.drawer = drawer; - this.usePresentationTimeStamp = usePresentationTimeStamp; - - eglBase = eglThread.createEglBaseWithSharedConnection(); - eglThread.getHandler().post(eglSurfaceCreationRunnable); - - final long currentTimeNs = System.nanoTime(); - resetStatistics(currentTimeNs); - - eglThread.getHandler().postDelayed( - logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC)); - } - } - /** * 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 @@ -203,9 +207,46 @@ public class EglRenderer implements VideoSink { */ public void init(@Nullable final EglBase.Context sharedContext, final int[] configAttributes, RendererCommon.GlDrawer drawer, boolean usePresentationTimeStamp) { - EglThread thread = - EglThread.create(/* releaseMonitor= */ null, sharedContext, configAttributes); - init(thread, drawer, usePresentationTimeStamp); + synchronized (handlerLock) { + if (renderThreadHandler != null) { + throw new IllegalStateException(name + "Already initialized"); + } + logD("Initializing EglRenderer"); + this.drawer = drawer; + this.usePresentationTimeStamp = usePresentationTimeStamp; + + final HandlerThread renderThread = new HandlerThread(name + "EglRenderer"); + renderThread.start(); + renderThreadHandler = + new HandlerWithExceptionCallback(renderThread.getLooper(), new Runnable() { + @Override + public void run() { + synchronized (handlerLock) { + renderThreadHandler = null; + } + } + }); + // 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, () -> { + // 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 = EglBase.createEgl10(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)); + } } /** @@ -240,15 +281,14 @@ public class EglRenderer implements VideoSink { public void release() { logD("Releasing."); final CountDownLatch eglCleanupBarrier = new CountDownLatch(1); - synchronized (threadLock) { - if (eglThread == null) { + synchronized (handlerLock) { + if (renderThreadHandler == null) { logD("Already released"); return; } - eglThread.getHandler().removeCallbacks(logStatisticsRunnable); - + renderThreadHandler.removeCallbacks(logStatisticsRunnable); // Release EGL and GL resources on render thread. - eglThread.getHandler().postAtFrontOfQueue(() -> { + renderThreadHandler.postAtFrontOfQueue(() -> { // Detach current shader program. synchronized (EglBase.lock) { GLES20.glUseProgram(/* program= */ 0); @@ -259,19 +299,23 @@ public class EglRenderer implements VideoSink { } frameDrawer.release(); bitmapTextureFramebuffer.release(); - if (eglBase != null) { + logD("eglBase detach and release."); + eglBase.detachCurrent(); eglBase.release(); eglBase = null; } - frameListeners.clear(); eglCleanupBarrier.countDown(); }); - + final Looper renderLooper = renderThreadHandler.getLooper(); + // TODO(magjed): Replace this post() with renderLooper.quitSafely() when API support >= 18. + renderThreadHandler.post(() -> { + logD("Quitting render thread."); + renderLooper.quit(); + }); // Don't accept any more frames or messages to the render thread. - eglThread.release(); - eglThread = null; + renderThreadHandler = null; } // Make sure the EGL/GL cleanup posted above is executed. ThreadUtils.awaitUninterruptibly(eglCleanupBarrier); @@ -299,9 +343,9 @@ public class EglRenderer implements VideoSink { } public void printStackTrace() { - synchronized (threadLock) { + synchronized (handlerLock) { final Thread renderThread = - (eglThread == null) ? null : eglThread.getHandler().getLooper().getThread(); + (renderThreadHandler == null) ? null : renderThreadHandler.getLooper().getThread(); if (renderThread != null) { final StackTraceElement[] renderStackTrace = renderThread.getStackTrace(); if (renderStackTrace.length > 0) { @@ -431,11 +475,11 @@ public class EglRenderer implements VideoSink { */ public void removeFrameListener(final FrameListener listener) { final CountDownLatch latch = new CountDownLatch(1); - synchronized (threadLock) { - if (eglThread == null) { + synchronized (handlerLock) { + if (renderThreadHandler == null) { return; } - if (Thread.currentThread() == eglThread.getHandler().getLooper().getThread()) { + if (Thread.currentThread() == renderThreadHandler.getLooper().getThread()) { throw new RuntimeException("removeFrameListener must not be called on the render thread."); } postToRenderThread(() -> { @@ -463,8 +507,8 @@ public class EglRenderer implements VideoSink { ++framesReceived; } final boolean dropOldFrame; - synchronized (threadLock) { - if (eglThread == null) { + synchronized (handlerLock) { + if (renderThreadHandler == null) { logD("Dropping frame - Not initialized or already released."); return; } @@ -475,7 +519,7 @@ public class EglRenderer implements VideoSink { } pendingFrame = frame; pendingFrame.retain(); - eglThread.getHandler().post(this::renderFrameOnRenderThread); + renderThreadHandler.post(this ::renderFrameOnRenderThread); } } if (dropOldFrame) { @@ -492,11 +536,12 @@ public class EglRenderer implements VideoSink { // Ensure that the render thread is no longer touching the Surface before returning from this // function. eglSurfaceCreationRunnable.setSurface(null /* surface */); - synchronized (threadLock) { - if (eglThread != null) { - eglThread.getHandler().removeCallbacks(eglSurfaceCreationRunnable); - eglThread.getHandler().postAtFrontOfQueue(() -> { + synchronized (handlerLock) { + if (renderThreadHandler != null) { + renderThreadHandler.removeCallbacks(eglSurfaceCreationRunnable); + renderThreadHandler.postAtFrontOfQueue(() -> { if (eglBase != null) { + eglBase.detachCurrent(); eglBase.releaseSurface(); } completionCallback.run(); @@ -511,9 +556,9 @@ public class EglRenderer implements VideoSink { * Private helper function to post tasks safely. */ private void postToRenderThread(Runnable runnable) { - synchronized (threadLock) { - if (eglThread != null) { - eglThread.getHandler().post(runnable); + synchronized (handlerLock) { + if (renderThreadHandler != null) { + renderThreadHandler.post(runnable); } } } @@ -521,7 +566,6 @@ public class EglRenderer implements VideoSink { private void clearSurfaceOnRenderThread(float r, float g, float b, float a) { if (eglBase != null && eglBase.hasSurface()) { logD("clearSurface"); - eglBase.makeCurrent(); GLES20.glClearColor(r, g, b, a); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); eglBase.swapBuffers(); @@ -539,11 +583,11 @@ public class EglRenderer implements VideoSink { * Post a task to clear the surface to a specific color. */ public void clearImage(final float r, final float g, final float b, final float a) { - synchronized (threadLock) { - if (eglThread == null) { + synchronized (handlerLock) { + if (renderThreadHandler == null) { return; } - eglThread.getHandler().postAtFrontOfQueue(() -> clearSurfaceOnRenderThread(r, g, b, a)); + renderThreadHandler.postAtFrontOfQueue(() -> clearSurfaceOnRenderThread(r, g, b, a)); } } @@ -565,8 +609,6 @@ public class EglRenderer implements VideoSink { frame.release(); return; } - eglBase.makeCurrent(); - // Check if fps reduction is active. final boolean shouldRenderFrame; synchronized (fpsReductionLock) {