Adopt EglThread in EglRenderer once again.
The regression obseverved on Samung devices the last time was caused by the not detaching the context/surface prior to releasing an EGLSurface or EGLContext. This was fine on most devices but obviously not all. Bug: b/225229697 Change-Id: I1849c772f3ed3e8819c748d997e5261289c4b2bc Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/321842 Commit-Queue: Linus Nilsson <lnilsson@webrtc.org> Reviewed-by: Zoé Lepaul <xalep@webrtc.org> Cr-Commit-Position: refs/heads/main@{#40844}
This commit is contained in:
parent
7d3eb10c7e
commit
dd15070b45
@ -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",
|
||||
|
||||
@ -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<FrameListenerAndParams> 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) {
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="23" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
@ -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][];
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user