diff --git a/sdk/android/BUILD.gn b/sdk/android/BUILD.gn index 041f4871d1..9aae8b9aa5 100644 --- a/sdk/android/BUILD.gn +++ b/sdk/android/BUILD.gn @@ -1476,7 +1476,7 @@ if (is_android) { apk_name = "android_instrumentation_test_apk" android_manifest = "instrumentationtests/AndroidManifest.xml" min_sdk_version = 21 - target_sdk_version = 21 + target_sdk_version = 23 sources = [ "instrumentationtests/src/org/webrtc/AndroidVideoDecoderInstrumentationTest.java", diff --git a/sdk/android/api/org/webrtc/EglRenderer.java b/sdk/android/api/org/webrtc/EglRenderer.java index 5ab0868ef3..202094a092 100644 --- a/sdk/android/api/org/webrtc/EglRenderer.java +++ b/sdk/android/api/org/webrtc/EglRenderer.java @@ -14,11 +14,8 @@ 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; @@ -86,35 +83,20 @@ 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; - // `renderThreadHandler` is a handler for communicating with `renderThread`, and is synchronized - // on `handlerLock`. - private final Object handlerLock = new Object(); - @Nullable private Handler renderThreadHandler; + // `eglThread` is used for rendering, and is synchronized on `threadLock`. + private final Object threadLock = new Object(); + @GuardedBy("threadLock") @Nullable private EglThread eglThread; + + private final Runnable eglExceptionCallback = new Runnable() { + @Override + public void run() { + synchronized (threadLock) { + eglThread = null; + } + } + }; private final ArrayList frameListeners = new ArrayList<>(); @@ -172,10 +154,10 @@ public class EglRenderer implements VideoSink { @Override public void run() { logStatistics(); - synchronized (handlerLock) { - if (renderThreadHandler != null) { - renderThreadHandler.removeCallbacks(logStatisticsRunnable); - renderThreadHandler.postDelayed( + synchronized (threadLock) { + if (eglThread != null) { + eglThread.getHandler().removeCallbacks(logStatisticsRunnable); + eglThread.getHandler().postDelayed( logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC)); } } @@ -185,8 +167,8 @@ public class EglRenderer implements VideoSink { private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation(); /** - * 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. + * Standard constructor. The name will be included when logging. In order to render something, + * you must first call init() and createEglSurface. */ public EglRenderer(String name) { this(name, new VideoFrameDrawer()); @@ -197,6 +179,31 @@ 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; + + eglThread.addExceptionCallback(eglExceptionCallback); + + 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 @@ -207,46 +214,9 @@ public class EglRenderer implements VideoSink { */ public void init(@Nullable final EglBase.Context sharedContext, final int[] configAttributes, RendererCommon.GlDrawer drawer, boolean 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)); - } + EglThread thread = + EglThread.create(/* releaseMonitor= */ null, sharedContext, configAttributes); + init(thread, drawer, usePresentationTimeStamp); } /** @@ -281,14 +251,16 @@ public class EglRenderer implements VideoSink { public void release() { logD("Releasing."); final CountDownLatch eglCleanupBarrier = new CountDownLatch(1); - synchronized (handlerLock) { - if (renderThreadHandler == null) { + synchronized (threadLock) { + if (eglThread == null) { logD("Already released"); return; } - renderThreadHandler.removeCallbacks(logStatisticsRunnable); + eglThread.getHandler().removeCallbacks(logStatisticsRunnable); + eglThread.removeExceptionCallback(eglExceptionCallback); + // Release EGL and GL resources on render thread. - renderThreadHandler.postAtFrontOfQueue(() -> { + eglThread.getHandler().postAtFrontOfQueue(() -> { // Detach current shader program. synchronized (EglBase.lock) { GLES20.glUseProgram(/* program= */ 0); @@ -299,23 +271,21 @@ 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. - renderThreadHandler = null; + eglThread.release(); + eglThread = null; } // Make sure the EGL/GL cleanup posted above is executed. ThreadUtils.awaitUninterruptibly(eglCleanupBarrier); @@ -343,9 +313,9 @@ public class EglRenderer implements VideoSink { } public void printStackTrace() { - synchronized (handlerLock) { + synchronized (threadLock) { final Thread renderThread = - (renderThreadHandler == null) ? null : renderThreadHandler.getLooper().getThread(); + (eglThread == null) ? null : eglThread.getHandler().getLooper().getThread(); if (renderThread != null) { final StackTraceElement[] renderStackTrace = renderThread.getStackTrace(); if (renderStackTrace.length > 0) { @@ -475,11 +445,11 @@ public class EglRenderer implements VideoSink { */ public void removeFrameListener(final FrameListener listener) { final CountDownLatch latch = new CountDownLatch(1); - synchronized (handlerLock) { - if (renderThreadHandler == null) { + synchronized (threadLock) { + if (eglThread == null) { return; } - if (Thread.currentThread() == renderThreadHandler.getLooper().getThread()) { + if (Thread.currentThread() == eglThread.getHandler().getLooper().getThread()) { throw new RuntimeException("removeFrameListener must not be called on the render thread."); } postToRenderThread(() -> { @@ -507,8 +477,8 @@ public class EglRenderer implements VideoSink { ++framesReceived; } final boolean dropOldFrame; - synchronized (handlerLock) { - if (renderThreadHandler == null) { + synchronized (threadLock) { + if (eglThread == null) { logD("Dropping frame - Not initialized or already released."); return; } @@ -519,7 +489,7 @@ public class EglRenderer implements VideoSink { } pendingFrame = frame; pendingFrame.retain(); - renderThreadHandler.post(this ::renderFrameOnRenderThread); + eglThread.getHandler().post(this::renderFrameOnRenderThread); } } if (dropOldFrame) { @@ -536,10 +506,10 @@ 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 (handlerLock) { - if (renderThreadHandler != null) { - renderThreadHandler.removeCallbacks(eglSurfaceCreationRunnable); - renderThreadHandler.postAtFrontOfQueue(() -> { + synchronized (threadLock) { + if (eglThread != null) { + eglThread.getHandler().removeCallbacks(eglSurfaceCreationRunnable); + eglThread.getHandler().postAtFrontOfQueue(() -> { if (eglBase != null) { eglBase.detachCurrent(); eglBase.releaseSurface(); @@ -556,9 +526,9 @@ public class EglRenderer implements VideoSink { * Private helper function to post tasks safely. */ private void postToRenderThread(Runnable runnable) { - synchronized (handlerLock) { - if (renderThreadHandler != null) { - renderThreadHandler.post(runnable); + synchronized (threadLock) { + if (eglThread != null) { + eglThread.getHandler().post(runnable); } } } @@ -566,6 +536,7 @@ 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(); @@ -583,11 +554,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 (handlerLock) { - if (renderThreadHandler == null) { + synchronized (threadLock) { + if (eglThread == null) { return; } - renderThreadHandler.postAtFrontOfQueue(() -> clearSurfaceOnRenderThread(r, g, b, a)); + eglThread.getHandler().postAtFrontOfQueue(() -> clearSurfaceOnRenderThread(r, g, b, a)); } } @@ -609,6 +580,8 @@ public class EglRenderer implements VideoSink { frame.release(); return; } + eglBase.makeCurrent(); + // Check if fps reduction is active. final boolean shouldRenderFrame; synchronized (fpsReductionLock) { diff --git a/sdk/android/instrumentationtests/AndroidManifest.xml b/sdk/android/instrumentationtests/AndroidManifest.xml index 55028da703..445752df05 100644 --- a/sdk/android/instrumentationtests/AndroidManifest.xml +++ b/sdk/android/instrumentationtests/AndroidManifest.xml @@ -16,7 +16,7 @@ - + diff --git a/sdk/android/instrumentationtests/src/org/webrtc/EglRendererTest.java b/sdk/android/instrumentationtests/src/org/webrtc/EglRendererTest.java index 3748cc3f3a..04e4ab47e8 100644 --- a/sdk/android/instrumentationtests/src/org/webrtc/EglRendererTest.java +++ b/sdk/android/instrumentationtests/src/org/webrtc/EglRendererTest.java @@ -103,6 +103,7 @@ public class EglRendererTest { final TestFrameListener testFrameListener = new TestFrameListener(); EglRenderer eglRenderer; + EglThread eglThread; CountDownLatch surfaceReadyLatch = new CountDownLatch(1); int oesTextureId; SurfaceTexture surfaceTexture; @@ -113,8 +114,10 @@ public class EglRendererTest { .builder(InstrumentationRegistry.getTargetContext()) .setNativeLibraryName(TestConstants.NATIVE_LIBRARY) .createInitializationOptions()); + eglThread = + EglThread.create(null /* releaseMonitor */, null /* sharedContext */, EglBase.CONFIG_RGBA); eglRenderer = new EglRenderer("TestRenderer: "); - eglRenderer.init(null /* sharedContext */, EglBase.CONFIG_RGBA, new GlRectDrawer()); + eglRenderer.init(eglThread, new GlRectDrawer(), false /* usePresentationTimeStamp */); oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); surfaceTexture = new SurfaceTexture(oesTextureId); surfaceTexture.setDefaultBufferSize(1 /* width */, 1 /* height */); @@ -244,6 +247,12 @@ public class EglRendererTest { } } + private void drainRenderThread() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + eglThread.getHandler().post(() -> latch.countDown()); + latch.await(); + } + /** Tells eglRenderer to render test frame with given index. */ private void feedFrame(int i) { final VideoFrame.I420Buffer buffer = JavaI420Buffer.wrap(TEST_FRAME_WIDTH, TEST_FRAME_HEIGHT, @@ -350,6 +359,22 @@ public class EglRendererTest { assertFalse(testFrameListener.waitForBitmap(RENDER_WAIT_MS)); } + @Test + @SmallTest + public void testRecreateSurface() throws Exception { + // Make sure the EGLSurface has been created. + drainRenderThread(); + + // Relase the surface and wait for completion. + CountDownLatch latch = new CountDownLatch(1); + eglRenderer.releaseEglSurface(() -> latch.countDown()); + latch.await(); + + // Recreate the surface. + eglRenderer.createEglSurface(surfaceTexture); + drainRenderThread(); + } + private static ByteBuffer[][] copyTestDataToDirectByteBuffers(byte[][][] testData) { final ByteBuffer[][] result = new ByteBuffer[testData.length][];