blob: ee29da85ad9e2d4b02365c6e13043e79f3028a6c [file] [log] [blame]
/*
* Copyright 2015 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.graphics.SurfaceTexture;
import android.opengl.GLES20;
import android.os.SystemClock;
import android.test.ActivityTestCase;
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 {
/**
* Mock texture listener with blocking wait functionality.
*/
public static final class MockTextureListener
implements SurfaceTextureHelper.OnTextureFrameAvailableListener {
public int oesTextureId;
public float[] transformMatrix;
private boolean hasNewFrame = false;
// Thread where frames are expected to be received on.
private final Thread expectedThread;
MockTextureListener() {
this.expectedThread = null;
}
MockTextureListener(Thread expectedThread) {
this.expectedThread = expectedThread;
}
@Override
public synchronized void onTextureFrameAvailable(
int oesTextureId, float[] transformMatrix, long timestampNs) {
if (expectedThread != null && Thread.currentThread() != expectedThread) {
throw new IllegalStateException("onTextureFrameAvailable called on wrong thread.");
}
this.oesTextureId = oesTextureId;
this.transformMatrix = transformMatrix;
hasNewFrame = true;
notifyAll();
}
/**
* Wait indefinitely for a new frame.
*/
public synchronized void waitForNewFrame() throws InterruptedException {
while (!hasNewFrame) {
wait();
}
hasNewFrame = false;
}
/**
* Wait for a new frame, or until the specified timeout elapses. Returns true if a new frame was
* received before the timeout.
*/
public synchronized boolean waitForNewFrame(final long timeoutMs) throws InterruptedException {
final long startTimeMs = SystemClock.elapsedRealtime();
long timeRemainingMs = timeoutMs;
while (!hasNewFrame && timeRemainingMs > 0) {
wait(timeRemainingMs);
final long elapsedTimeMs = SystemClock.elapsedRealtime() - startTimeMs;
timeRemainingMs = timeoutMs - elapsedTimeMs;
}
final boolean didReceiveFrame = hasNewFrame;
hasNewFrame = false;
return didReceiveFrame;
}
}
/** Assert that two integers are close, with difference at most
* {@code threshold}. */
public static void assertClose(int threshold, int expected, int actual) {
if (Math.abs(expected - actual) <= threshold)
return;
failNotEquals("Not close enough, threshold " + threshold, expected, actual);
}
/**
* Test normal use by receiving three uniform texture frames. Texture frames are returned as early
* as possible. The texture pixel values are inspected by drawing the texture frame to a pixel
* buffer and reading it back with glReadPixels().
*/
@MediumTest
public static void testThreeConstantColorFrames() throws InterruptedException {
final int width = 16;
final int height = 16;
// Create EGL base with a pixel buffer as display output.
final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER);
eglBase.createPbufferSurface(width, height);
final GlRectDrawer drawer = new GlRectDrawer();
// Create SurfaceTextureHelper and listener.
final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(
"SurfaceTextureHelper test" /* threadName */, eglBase.getEglBaseContext());
final MockTextureListener listener = new MockTextureListener();
surfaceTextureHelper.startListening(listener);
surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height);
// Create resources for stubbing an OES texture producer. |eglOesBase| has the SurfaceTexture in
// |surfaceTextureHelper| as the target EGLSurface.
final EglBase eglOesBase = EglBase.create(eglBase.getEglBaseContext(), EglBase.CONFIG_PLAIN);
eglOesBase.createSurface(surfaceTextureHelper.getSurfaceTexture());
assertEquals(eglOesBase.surfaceWidth(), width);
assertEquals(eglOesBase.surfaceHeight(), height);
final int red[] = new int[] {79, 144, 185};
final int green[] = new int[] {66, 210, 162};
final int blue[] = new int[] {161, 117, 158};
// Draw three frames.
for (int i = 0; i < 3; ++i) {
// Draw a constant color frame onto the SurfaceTexture.
eglOesBase.makeCurrent();
GLES20.glClearColor(red[i] / 255.0f, green[i] / 255.0f, blue[i] / 255.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// swapBuffers() will ultimately trigger onTextureFrameAvailable().
eglOesBase.swapBuffers();
// Wait for an OES texture to arrive and draw it onto the pixel buffer.
listener.waitForNewFrame();
eglBase.makeCurrent();
drawer.drawOes(
listener.oesTextureId, listener.transformMatrix, width, height, 0, 0, width, height);
surfaceTextureHelper.returnTextureFrame();
// Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g.
// Nexus 9.
final ByteBuffer rgbaData = ByteBuffer.allocateDirect(width * height * 4);
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaData);
GlUtil.checkNoGLES2Error("glReadPixels");
// Assert rendered image is expected constant color.
while (rgbaData.hasRemaining()) {
assertEquals(rgbaData.get() & 0xFF, red[i]);
assertEquals(rgbaData.get() & 0xFF, green[i]);
assertEquals(rgbaData.get() & 0xFF, blue[i]);
assertEquals(rgbaData.get() & 0xFF, 255);
}
}
drawer.release();
surfaceTextureHelper.dispose();
eglBase.release();
}
/**
* 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().
*/
@MediumTest
public static void testLateReturnFrame() throws InterruptedException {
final int width = 16;
final int height = 16;
// Create EGL base with a pixel buffer as display output.
final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER);
eglBase.createPbufferSurface(width, height);
// Create SurfaceTextureHelper and listener.
final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(
"SurfaceTextureHelper test" /* threadName */, eglBase.getEglBaseContext());
final MockTextureListener listener = new MockTextureListener();
surfaceTextureHelper.startListening(listener);
surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height);
// Create resources for stubbing an OES texture producer. |eglOesBase| has the SurfaceTexture in
// |surfaceTextureHelper| as the target EGLSurface.
final EglBase eglOesBase = EglBase.create(eglBase.getEglBaseContext(), EglBase.CONFIG_PLAIN);
eglOesBase.createSurface(surfaceTextureHelper.getSurfaceTexture());
assertEquals(eglOesBase.surfaceWidth(), width);
assertEquals(eglOesBase.surfaceHeight(), height);
final int red = 79;
final int green = 66;
final int blue = 161;
// Draw a constant color frame onto the SurfaceTexture.
eglOesBase.makeCurrent();
GLES20.glClearColor(red / 255.0f, green / 255.0f, blue / 255.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// swapBuffers() will ultimately trigger onTextureFrameAvailable().
eglOesBase.swapBuffers();
eglOesBase.release();
// Wait for OES texture frame.
listener.waitForNewFrame();
// Diconnect while holding the frame.
surfaceTextureHelper.dispose();
// Draw the pending texture frame onto the pixel buffer.
eglBase.makeCurrent();
final GlRectDrawer drawer = new GlRectDrawer();
drawer.drawOes(
listener.oesTextureId, listener.transformMatrix, width, height, 0, 0, width, height);
drawer.release();
// Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g. Nexus 9.
final ByteBuffer rgbaData = ByteBuffer.allocateDirect(width * height * 4);
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaData);
GlUtil.checkNoGLES2Error("glReadPixels");
eglBase.release();
// Assert rendered image is expected constant color.
while (rgbaData.hasRemaining()) {
assertEquals(rgbaData.get() & 0xFF, red);
assertEquals(rgbaData.get() & 0xFF, green);
assertEquals(rgbaData.get() & 0xFF, blue);
assertEquals(rgbaData.get() & 0xFF, 255);
}
// Late frame return after everything has been disposed and released.
surfaceTextureHelper.returnTextureFrame();
}
/**
* Test disposing the SurfaceTextureHelper, but keep trying to produce more texture frames. No
* frames should be delivered to the listener.
*/
@MediumTest
public static void testDispose() throws InterruptedException {
// Create SurfaceTextureHelper and listener.
final SurfaceTextureHelper surfaceTextureHelper =
SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, 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();
// 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 disposed has been called.
// Assert that no OES texture was delivered.
assertFalse(listener.waitForNewFrame(500));
eglBase.release();
}
/**
* 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 testDisposeImmediately() {
final SurfaceTextureHelper surfaceTextureHelper =
SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, null);
surfaceTextureHelper.dispose();
}
/**
* 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("SurfaceTextureHelper test" /* threadName */, 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.
surfaceTextureHelper.stopListening();
// 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("SurfaceTextureHelper test" /* threadName */, null);
final MockTextureListener listener = new MockTextureListener();
surfaceTextureHelper.startListening(listener);
surfaceTextureHelper.stopListening();
surfaceTextureHelper.dispose();
}
/**
* Test stopListening() immediately after the SurfaceTextureHelper has been setup on the handler
* thread.
*/
@SmallTest
public static void testStopListeningImmediatelyOnHandlerThread() throws InterruptedException {
final SurfaceTextureHelper surfaceTextureHelper =
SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, null);
final MockTextureListener listener = new MockTextureListener();
final CountDownLatch stopListeningBarrier = new CountDownLatch(1);
final CountDownLatch stopListeningBarrierDone = new CountDownLatch(1);
// Start by posting to the handler thread to keep it occupied.
surfaceTextureHelper.getHandler().post(new Runnable() {
@Override
public void run() {
ThreadUtils.awaitUninterruptibly(stopListeningBarrier);
surfaceTextureHelper.stopListening();
stopListeningBarrierDone.countDown();
}
});
// startListening() is asynchronous and will post to the occupied handler thread.
surfaceTextureHelper.startListening(listener);
// Wait for stopListening() to be called on the handler thread.
stopListeningBarrier.countDown();
stopListeningBarrierDone.await();
// Wait until handler thread is idle to try to catch late startListening() call.
final CountDownLatch barrier = new CountDownLatch(1);
surfaceTextureHelper.getHandler().post(new Runnable() {
@Override
public void run() {
barrier.countDown();
}
});
ThreadUtils.awaitUninterruptibly(barrier);
// Previous startListening() call should never have taken place and it should be ok to call it
// again.
surfaceTextureHelper.startListening(listener);
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("SurfaceTextureHelper test" /* threadName */, 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.
surfaceTextureHelper.stopListening();
// 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
public static void testTexturetoYUV() throws InterruptedException {
final int width = 16;
final int height = 16;
final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN);
// Create SurfaceTextureHelper and listener.
final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(
"SurfaceTextureHelper test" /* threadName */, eglBase.getEglBaseContext());
final MockTextureListener listener = new MockTextureListener();
surfaceTextureHelper.startListening(listener);
surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height);
// Create resources for stubbing an OES texture producer. |eglBase| has the SurfaceTexture in
// |surfaceTextureHelper| as the target EGLSurface.
eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture());
assertEquals(eglBase.surfaceWidth(), width);
assertEquals(eglBase.surfaceHeight(), height);
final int red[] = new int[] {79, 144, 185};
final int green[] = new int[] {66, 210, 162};
final int blue[] = new int[] {161, 117, 158};
final int ref_y[] = new int[] {81, 180, 168};
final int ref_u[] = new int[] {173, 93, 122};
final int ref_v[] = new int[] {127, 103, 140};
// Draw three frames.
for (int i = 0; i < 3; ++i) {
// Draw a constant color frame onto the SurfaceTexture.
eglBase.makeCurrent();
GLES20.glClearColor(red[i] / 255.0f, green[i] / 255.0f, blue[i] / 255.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// swapBuffers() will ultimately trigger onTextureFrameAvailable().
eglBase.swapBuffers();
// Wait for an OES texture to arrive.
listener.waitForNewFrame();
// Memory layout: Lines are 16 bytes. First 16 lines are
// the Y data. These are followed by 8 lines with 8 bytes of U
// data on the left and 8 bytes of V data on the right.
//
// Offset
// 0 YYYYYYYY YYYYYYYY
// 16 YYYYYYYY YYYYYYYY
// ...
// 240 YYYYYYYY YYYYYYYY
// 256 UUUUUUUU VVVVVVVV
// 272 UUUUUUUU VVVVVVVV
// ...
// 368 UUUUUUUU VVVVVVVV
// 384 buffer end
ByteBuffer buffer = ByteBuffer.allocateDirect(width * height * 3 / 2);
surfaceTextureHelper.textureToYUV(
buffer, width, height, width, listener.oesTextureId, listener.transformMatrix);
surfaceTextureHelper.returnTextureFrame();
// Allow off-by-one differences due to different rounding.
while (buffer.position() < width * height) {
assertClose(1, buffer.get() & 0xff, ref_y[i]);
}
while (buffer.hasRemaining()) {
if (buffer.position() % width < width / 2)
assertClose(1, buffer.get() & 0xff, ref_u[i]);
else
assertClose(1, buffer.get() & 0xff, ref_v[i]);
}
}
surfaceTextureHelper.dispose();
eglBase.release();
}
}