diff --git a/webrtc/api/androidtests/src/org/webrtc/GlRectDrawerTest.java b/webrtc/api/androidtests/src/org/webrtc/GlRectDrawerTest.java index 4b0e2a28be..3a69da1cae 100644 --- a/webrtc/api/androidtests/src/org/webrtc/GlRectDrawerTest.java +++ b/webrtc/api/androidtests/src/org/webrtc/GlRectDrawerTest.java @@ -256,7 +256,7 @@ public final class GlRectDrawerTest extends ActivityTestCase { eglBase.getEglBaseContext(), surfaceTextureHelper.getSurfaceTexture(), WIDTH, HEIGHT); final SurfaceTextureHelperTest.MockTextureListener listener = new SurfaceTextureHelperTest.MockTextureListener(); - surfaceTextureHelper.setListener(listener); + surfaceTextureHelper.startListening(listener); // Create RGB byte buffer plane with random content. final ByteBuffer rgbPlane = ByteBuffer.allocateDirect(WIDTH * HEIGHT * 3); @@ -284,7 +284,7 @@ public final class GlRectDrawerTest extends ActivityTestCase { drawer.release(); surfaceTextureHelper.returnTextureFrame(); oesProducer.release(); - surfaceTextureHelper.disconnect(); + surfaceTextureHelper.dispose(); eglBase.release(); } } diff --git a/webrtc/api/androidtests/src/org/webrtc/SurfaceTextureHelperTest.java b/webrtc/api/androidtests/src/org/webrtc/SurfaceTextureHelperTest.java index f2a8a6fc66..8343772c1c 100644 --- a/webrtc/api/androidtests/src/org/webrtc/SurfaceTextureHelperTest.java +++ b/webrtc/api/androidtests/src/org/webrtc/SurfaceTextureHelperTest.java @@ -17,6 +17,7 @@ import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; public final class SurfaceTextureHelperTest extends ActivityTestCase { /** @@ -104,7 +105,7 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(eglBase.getEglBaseContext()); final MockTextureListener listener = new MockTextureListener(); - surfaceTextureHelper.setListener(listener); + surfaceTextureHelper.startListening(listener); surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height); // Create resources for stubbing an OES texture producer. |eglOesBase| has the SurfaceTexture in @@ -150,12 +151,12 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { } drawer.release(); - surfaceTextureHelper.disconnect(); + surfaceTextureHelper.dispose(); eglBase.release(); } /** - * Test disconnecting the SurfaceTextureHelper while holding a pending texture frame. The pending + * Test disposing the SurfaceTextureHelper while holding a pending texture frame. The pending * texture frame should still be valid, and this is tested by drawing the texture frame to a pixel * buffer and reading it back with glReadPixels(). */ @@ -171,7 +172,7 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(eglBase.getEglBaseContext()); final MockTextureListener listener = new MockTextureListener(); - surfaceTextureHelper.setListener(listener); + surfaceTextureHelper.startListening(listener); surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height); // Create resources for stubbing an OES texture producer. |eglOesBase| has the SurfaceTexture in @@ -196,7 +197,7 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { // Wait for OES texture frame. listener.waitForNewFrame(); // Diconnect while holding the frame. - surfaceTextureHelper.disconnect(); + surfaceTextureHelper.dispose(); // Draw the pending texture frame onto the pixel buffer. eglBase.makeCurrent(); @@ -217,21 +218,21 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { assertEquals(rgbaData.get() & 0xFF, blue); assertEquals(rgbaData.get() & 0xFF, 255); } - // Late frame return after everything has been disconnected and released. + // Late frame return after everything has been disposed and released. surfaceTextureHelper.returnTextureFrame(); } /** - * Test disconnecting the SurfaceTextureHelper, but keep trying to produce more texture frames. No + * Test disposing the SurfaceTextureHelper, but keep trying to produce more texture frames. No * frames should be delivered to the listener. */ @MediumTest - public static void testDisconnect() throws InterruptedException { + public static void testDispose() throws InterruptedException { // Create SurfaceTextureHelper and listener. final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(null); final MockTextureListener listener = new MockTextureListener(); - surfaceTextureHelper.setListener(listener); + surfaceTextureHelper.startListening(listener); // Create EglBase with the SurfaceTexture as target EGLSurface. final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); @@ -245,13 +246,13 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { listener.waitForNewFrame(); surfaceTextureHelper.returnTextureFrame(); - // Disconnect - we should not receive any textures after this. - surfaceTextureHelper.disconnect(); + // Dispose - we should not receive any textures after this. + surfaceTextureHelper.dispose(); // Draw one frame. GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); eglBase.swapBuffers(); - // swapBuffers() should not trigger onTextureFrameAvailable() because we are disconnected. + // swapBuffers() should not trigger onTextureFrameAvailable() because disposed has been called. // Assert that no OES texture was delivered. assertFalse(listener.waitForNewFrame(500)); @@ -259,14 +260,125 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { } /** - * Test disconnecting the SurfaceTextureHelper immediately after is has been setup to use a + * Test disposing the SurfaceTextureHelper immediately after is has been setup to use a * shared context. No frames should be delivered to the listener. */ @SmallTest - public static void testDisconnectImmediately() { + public static void testDisposeImmediately() { final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(null); - surfaceTextureHelper.disconnect(); + surfaceTextureHelper.dispose(); + } + + // Helper method to call stopListening() on correct thread. + private static void stopListeningOnHandlerThread(final SurfaceTextureHelper surfaceTextureHelper) + throws InterruptedException { + final CountDownLatch barrier = new CountDownLatch(1); + surfaceTextureHelper.getHandler().post(new Runnable() { + @Override + public void run() { + surfaceTextureHelper.stopListening(); + barrier.countDown(); + } + }); + barrier.await(); + } + + /** + * Call stopListening(), but keep trying to produce more texture frames. No frames should be + * delivered to the listener. + */ + @MediumTest + public static void testStopListening() throws InterruptedException { + // Create SurfaceTextureHelper and listener. + final SurfaceTextureHelper surfaceTextureHelper = + SurfaceTextureHelper.create(null); + final MockTextureListener listener = new MockTextureListener(); + surfaceTextureHelper.startListening(listener); + // Create EglBase with the SurfaceTexture as target EGLSurface. + final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); + eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); + eglBase.makeCurrent(); + // Assert no frame has been received yet. + assertFalse(listener.waitForNewFrame(1)); + // Draw and wait for one frame. + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + // swapBuffers() will ultimately trigger onTextureFrameAvailable(). + eglBase.swapBuffers(); + listener.waitForNewFrame(); + surfaceTextureHelper.returnTextureFrame(); + + // Stop listening - we should not receive any textures after this. + stopListeningOnHandlerThread(surfaceTextureHelper); + + // Draw one frame. + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + eglBase.swapBuffers(); + // swapBuffers() should not trigger onTextureFrameAvailable() because disposed has been called. + // Assert that no OES texture was delivered. + assertFalse(listener.waitForNewFrame(500)); + + surfaceTextureHelper.dispose(); + eglBase.release(); + } + + /** + * Test stopListening() immediately after the SurfaceTextureHelper has been setup. + */ + @SmallTest + public static void testStopListeningImmediately() throws InterruptedException { + final SurfaceTextureHelper surfaceTextureHelper = + SurfaceTextureHelper.create(null); + final MockTextureListener listener = new MockTextureListener(); + surfaceTextureHelper.startListening(listener); + stopListeningOnHandlerThread(surfaceTextureHelper); + surfaceTextureHelper.dispose(); + } + + /** + * Test calling startListening() with a new listener after stopListening() has been called. + */ + @MediumTest + public static void testRestartListeningWithNewListener() throws InterruptedException { + // Create SurfaceTextureHelper and listener. + final SurfaceTextureHelper surfaceTextureHelper = + SurfaceTextureHelper.create(null); + final MockTextureListener listener1 = new MockTextureListener(); + surfaceTextureHelper.startListening(listener1); + // Create EglBase with the SurfaceTexture as target EGLSurface. + final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); + eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); + eglBase.makeCurrent(); + // Assert no frame has been received yet. + assertFalse(listener1.waitForNewFrame(1)); + // Draw and wait for one frame. + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + // swapBuffers() will ultimately trigger onTextureFrameAvailable(). + eglBase.swapBuffers(); + listener1.waitForNewFrame(); + surfaceTextureHelper.returnTextureFrame(); + + // Stop listening - |listener1| should not receive any textures after this. + stopListeningOnHandlerThread(surfaceTextureHelper); + + // Connect different listener. + final MockTextureListener listener2 = new MockTextureListener(); + surfaceTextureHelper.startListening(listener2); + // Assert no frame has been received yet. + assertFalse(listener2.waitForNewFrame(1)); + + // Draw one frame. + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + eglBase.swapBuffers(); + + // Check that |listener2| received the frame, and not |listener1|. + listener2.waitForNewFrame(); + assertFalse(listener1.waitForNewFrame(1)); + + surfaceTextureHelper.returnTextureFrame(); + + surfaceTextureHelper.dispose(); + eglBase.release(); } @MediumTest @@ -280,7 +392,7 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(eglBase.getEglBaseContext()); final MockTextureListener listener = new MockTextureListener(); - surfaceTextureHelper.setListener(listener); + surfaceTextureHelper.startListening(listener); surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height); // Create resources for stubbing an OES texture producer. |eglBase| has the SurfaceTexture in @@ -342,7 +454,7 @@ public final class SurfaceTextureHelperTest extends ActivityTestCase { } } - surfaceTextureHelper.disconnect(); + surfaceTextureHelper.dispose(); eglBase.release(); } } diff --git a/webrtc/api/java/android/org/webrtc/SurfaceTextureHelper.java b/webrtc/api/java/android/org/webrtc/SurfaceTextureHelper.java index 1781047107..3f5617d420 100644 --- a/webrtc/api/java/android/org/webrtc/SurfaceTextureHelper.java +++ b/webrtc/api/java/android/org/webrtc/SurfaceTextureHelper.java @@ -28,8 +28,8 @@ import java.util.concurrent.TimeUnit; * Helper class to create and synchronize access to a SurfaceTexture. The caller will get notified * of new frames in onTextureFrameAvailable(), and should call returnTextureFrame() when done with * the frame. Only one texture frame can be in flight at once, so returnTextureFrame() must be - * called in order to receive a new frame. Call disconnect() to stop receiveing new frames and - * release all resources. + * called in order to receive a new frame. Call stopListening() to stop receiveing new frames. Call + * dispose to release all resources once the texture frame is returned. * Note that there is a C++ counter part of this class that optionally can be used. It is used for * wrapping texture frames into webrtc::VideoFrames and also handles calling returnTextureFrame() * when the webrtc::VideoFrame is no longer used. @@ -287,6 +287,7 @@ class SurfaceTextureHelper { private final int oesTextureId; private YuvConverter yuvConverter; + // These variables are only accessed from the |handler| thread. private OnTextureFrameAvailableListener listener; // The possible states of this class. private boolean hasPendingTexture = false; @@ -305,6 +306,13 @@ class SurfaceTextureHelper { oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); surfaceTexture = new SurfaceTexture(oesTextureId); + surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + hasPendingTexture = true; + tryDeliverTextureFrame(); + } + }); } private YuvConverter getYuvConverter() { @@ -320,23 +328,35 @@ class SurfaceTextureHelper { } /** - * Start to stream textures to the given |listener|. - * A Listener can only be set once. + * Start to stream textures to the given |listener|. If you need to change listener, you need to + * call stopListening() first. */ - public void setListener(OnTextureFrameAvailableListener listener) { + public void startListening(final OnTextureFrameAvailableListener listener) { if (this.listener != null) { throw new IllegalStateException("SurfaceTextureHelper listener has already been set."); } - this.listener = listener; - surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { + handler.post(new Runnable() { @Override - public void onFrameAvailable(SurfaceTexture surfaceTexture) { - hasPendingTexture = true; + public void run() { + SurfaceTextureHelper.this.listener = listener; + // May alredy have a pending frame - try delivering it. tryDeliverTextureFrame(); } }); } + /** + * Stop listening. The listener set in startListening() is guaranteded to not receive any more + * onTextureFrameAvailable() callbacks after this function returns. This function must be called + * on the getHandler() thread. + */ + public void stopListening() { + if (handler.getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException("Wrong thread."); + } + this.listener = null; + } + /** * Retrieve the underlying SurfaceTexture. The SurfaceTexture should be passed in to a video * producer such as a camera or decoder. @@ -347,7 +367,7 @@ class SurfaceTextureHelper { /** * Retrieve the handler that calls onTextureFrameAvailable(). This handler is valid until - * disconnect() is called. + * dispose() is called. */ public Handler getHandler() { return handler; @@ -380,7 +400,7 @@ class SurfaceTextureHelper { * stopped when the texture frame has been returned by a call to returnTextureFrame(). You are * guaranteed to not receive any more onTextureFrameAvailable() after this function returns. */ - public void disconnect() { + public void dispose() { if (handler.getLooper().getThread() == Thread.currentThread()) { isQuitting = true; if (!isTextureInUse) { @@ -413,7 +433,7 @@ class SurfaceTextureHelper { if (handler.getLooper().getThread() != Thread.currentThread()) { throw new IllegalStateException("Wrong thread."); } - if (isQuitting || !hasPendingTexture || isTextureInUse) { + if (isQuitting || !hasPendingTexture || isTextureInUse || listener == null) { return; } isTextureInUse = true; diff --git a/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java b/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java index a696805b2a..be6b3e26fe 100644 --- a/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java +++ b/webrtc/api/java/android/org/webrtc/VideoCapturerAndroid.java @@ -306,9 +306,6 @@ public class VideoCapturerAndroid implements surfaceHelper = SurfaceTextureHelper.create(sharedContext); cameraThreadHandler = surfaceHelper.getHandler(); cameraThread = cameraThreadHandler.getLooper().getThread(); - if (isCapturingToTexture) { - surfaceHelper.setListener(this); - } Logging.d(TAG, "VideoCapturerAndroid isCapturingToTexture : " + isCapturingToTexture); } @@ -352,7 +349,7 @@ public class VideoCapturerAndroid implements } } }); - surfaceHelper.disconnect(); + surfaceHelper.dispose(); cameraThread = null; } @@ -436,6 +433,9 @@ public class VideoCapturerAndroid implements camera.setErrorCallback(cameraErrorCallback); startPreviewOnCameraThread(width, height, framerate); frameObserver.onCapturerStarted(true); + if (isCapturingToTexture) { + surfaceHelper.startListening(this); + } // Start camera observer. cameraThreadHandler.postDelayed(cameraObserver, CAMERA_OBSERVER_PERIOD_MS); @@ -544,6 +544,8 @@ public class VideoCapturerAndroid implements final CountDownLatch barrier = new CountDownLatch(1); cameraThreadHandler.post(new Runnable() { @Override public void run() { + // Make sure onTextureFrameAvailable() is not called anymore. + surfaceHelper.stopListening(); stopCaptureOnCameraThread(); barrier.countDown(); } @@ -669,12 +671,10 @@ public class VideoCapturerAndroid implements @Override public void onTextureFrameAvailable( int oesTextureId, float[] transformMatrix, long timestampNs) { - checkIsOnCameraThread(); if (camera == null) { - // Camera is stopped, we need to return the buffer immediately. - surfaceHelper.returnTextureFrame(); - return; + throw new RuntimeException("onTextureFrameAvailable() called after stopCapture()."); } + checkIsOnCameraThread(); if (dropNextFrame) { surfaceHelper.returnTextureFrame(); dropNextFrame = false; diff --git a/webrtc/api/java/src/org/webrtc/MediaCodecVideoDecoder.java b/webrtc/api/java/src/org/webrtc/MediaCodecVideoDecoder.java index 26b1cf2491..fe2c7dc468 100644 --- a/webrtc/api/java/src/org/webrtc/MediaCodecVideoDecoder.java +++ b/webrtc/api/java/src/org/webrtc/MediaCodecVideoDecoder.java @@ -469,7 +469,7 @@ public class MediaCodecVideoDecoder { public TextureListener(SurfaceTextureHelper surfaceTextureHelper) { this.surfaceTextureHelper = surfaceTextureHelper; - surfaceTextureHelper.setListener(this); + surfaceTextureHelper.startListening(this); } public void addBufferToRender(DecodedOutputBuffer buffer) { @@ -525,10 +525,10 @@ public class MediaCodecVideoDecoder { } public void release() { - // SurfaceTextureHelper.disconnect() will block until any onTextureFrameAvailable() in - // progress is done. Therefore, the call to disconnect() must be outside any synchronized + // SurfaceTextureHelper.dispose() will block until any onTextureFrameAvailable() in + // progress is done. Therefore, the call to dispose() must be outside any synchronized // statement that is also used in the onTextureFrameAvailable() above to avoid deadlocks. - surfaceTextureHelper.disconnect(); + surfaceTextureHelper.dispose(); synchronized (newFrameLock) { if (renderedBuffer != null) { surfaceTextureHelper.returnTextureFrame();