1 /* 2 * Copyright 2015 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.webrtc; 12 13 import static org.junit.Assert.assertEquals; 14 import static org.junit.Assert.assertTrue; 15 import static org.junit.Assert.fail; 16 17 import android.opengl.GLES20; 18 import android.os.SystemClock; 19 import android.support.annotation.Nullable; 20 import android.support.test.filters.MediumTest; 21 import android.support.test.filters.SmallTest; 22 import java.nio.ByteBuffer; 23 import java.util.concurrent.CountDownLatch; 24 import org.chromium.base.test.BaseJUnit4ClassRunner; 25 import org.junit.Before; 26 import org.junit.Test; 27 import org.junit.runner.RunWith; 28 29 @RunWith(BaseJUnit4ClassRunner.class) 30 public class SurfaceTextureHelperTest { 31 /** 32 * Mock texture listener with blocking wait functionality. 33 */ 34 public static final class MockTextureListener implements VideoSink { 35 private final Object lock = new Object(); 36 private @Nullable VideoFrame.TextureBuffer textureBuffer; 37 // Thread where frames are expected to be received on. 38 private final @Nullable Thread expectedThread; 39 MockTextureListener()40 MockTextureListener() { 41 this.expectedThread = null; 42 } 43 MockTextureListener(Thread expectedThread)44 MockTextureListener(Thread expectedThread) { 45 this.expectedThread = expectedThread; 46 } 47 48 @Override onFrame(VideoFrame frame)49 public void onFrame(VideoFrame frame) { 50 if (expectedThread != null && Thread.currentThread() != expectedThread) { 51 throw new IllegalStateException("onTextureFrameAvailable called on wrong thread."); 52 } 53 synchronized (lock) { 54 this.textureBuffer = (VideoFrame.TextureBuffer) frame.getBuffer(); 55 textureBuffer.retain(); 56 lock.notifyAll(); 57 } 58 } 59 60 /** Wait indefinitely for a new textureBuffer. */ waitForTextureBuffer()61 public VideoFrame.TextureBuffer waitForTextureBuffer() throws InterruptedException { 62 synchronized (lock) { 63 while (true) { 64 final VideoFrame.TextureBuffer textureBufferToReturn = textureBuffer; 65 if (textureBufferToReturn != null) { 66 textureBuffer = null; 67 return textureBufferToReturn; 68 } 69 lock.wait(); 70 } 71 } 72 } 73 74 /** Make sure we get no frame in the specified time period. */ assertNoFrameIsDelivered(final long waitPeriodMs)75 public void assertNoFrameIsDelivered(final long waitPeriodMs) throws InterruptedException { 76 final long startTimeMs = SystemClock.elapsedRealtime(); 77 long timeRemainingMs = waitPeriodMs; 78 synchronized (lock) { 79 while (textureBuffer == null && timeRemainingMs > 0) { 80 lock.wait(timeRemainingMs); 81 final long elapsedTimeMs = SystemClock.elapsedRealtime() - startTimeMs; 82 timeRemainingMs = waitPeriodMs - elapsedTimeMs; 83 } 84 assertTrue(textureBuffer == null); 85 } 86 } 87 } 88 89 /** Assert that two integers are close, with difference at most 90 * {@code threshold}. */ assertClose(int threshold, int expected, int actual)91 public static void assertClose(int threshold, int expected, int actual) { 92 if (Math.abs(expected - actual) <= threshold) 93 return; 94 fail("Not close enough, threshold " + threshold + ". Expected: " + expected + " Actual: " 95 + actual); 96 } 97 98 @Before setUp()99 public void setUp() { 100 // Load the JNI library for textureToYuv. 101 NativeLibrary.initialize(new NativeLibrary.DefaultLoader(), TestConstants.NATIVE_LIBRARY); 102 } 103 104 /** 105 * Test normal use by receiving three uniform texture frames. Texture frames are returned as early 106 * as possible. The texture pixel values are inspected by drawing the texture frame to a pixel 107 * buffer and reading it back with glReadPixels(). 108 */ 109 @Test 110 @MediumTest testThreeConstantColorFrames()111 public void testThreeConstantColorFrames() throws InterruptedException { 112 final int width = 16; 113 final int height = 16; 114 // Create EGL base with a pixel buffer as display output. 115 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER); 116 eglBase.createPbufferSurface(width, height); 117 final GlRectDrawer drawer = new GlRectDrawer(); 118 119 // Create SurfaceTextureHelper and listener. 120 final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create( 121 "SurfaceTextureHelper test" /* threadName */, eglBase.getEglBaseContext()); 122 final MockTextureListener listener = new MockTextureListener(); 123 surfaceTextureHelper.startListening(listener); 124 surfaceTextureHelper.setTextureSize(width, height); 125 126 // Create resources for stubbing an OES texture producer. |eglOesBase| has the SurfaceTexture in 127 // |surfaceTextureHelper| as the target EGLSurface. 128 final EglBase eglOesBase = EglBase.create(eglBase.getEglBaseContext(), EglBase.CONFIG_PLAIN); 129 eglOesBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 130 assertEquals(eglOesBase.surfaceWidth(), width); 131 assertEquals(eglOesBase.surfaceHeight(), height); 132 133 final int red[] = new int[] {79, 144, 185}; 134 final int green[] = new int[] {66, 210, 162}; 135 final int blue[] = new int[] {161, 117, 158}; 136 // Draw three frames. 137 for (int i = 0; i < 3; ++i) { 138 // Draw a constant color frame onto the SurfaceTexture. 139 eglOesBase.makeCurrent(); 140 GLES20.glClearColor(red[i] / 255.0f, green[i] / 255.0f, blue[i] / 255.0f, 1.0f); 141 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 142 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 143 eglOesBase.swapBuffers(); 144 145 // Wait for an OES texture to arrive and draw it onto the pixel buffer. 146 final VideoFrame.TextureBuffer textureBuffer = listener.waitForTextureBuffer(); 147 eglBase.makeCurrent(); 148 drawer.drawOes(textureBuffer.getTextureId(), 149 RendererCommon.convertMatrixFromAndroidGraphicsMatrix(textureBuffer.getTransformMatrix()), 150 width, height, 0, 0, width, height); 151 textureBuffer.release(); 152 153 // Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g. 154 // Nexus 9. 155 final ByteBuffer rgbaData = ByteBuffer.allocateDirect(width * height * 4); 156 GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaData); 157 GlUtil.checkNoGLES2Error("glReadPixels"); 158 159 // Assert rendered image is expected constant color. 160 while (rgbaData.hasRemaining()) { 161 assertEquals(rgbaData.get() & 0xFF, red[i]); 162 assertEquals(rgbaData.get() & 0xFF, green[i]); 163 assertEquals(rgbaData.get() & 0xFF, blue[i]); 164 assertEquals(rgbaData.get() & 0xFF, 255); 165 } 166 } 167 168 drawer.release(); 169 surfaceTextureHelper.dispose(); 170 eglBase.release(); 171 } 172 173 /** 174 * Test disposing the SurfaceTextureHelper while holding a pending texture frame. The pending 175 * texture frame should still be valid, and this is tested by drawing the texture frame to a pixel 176 * buffer and reading it back with glReadPixels(). 177 */ 178 @Test 179 @MediumTest testLateReturnFrame()180 public void testLateReturnFrame() throws InterruptedException { 181 final int width = 16; 182 final int height = 16; 183 // Create EGL base with a pixel buffer as display output. 184 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER); 185 eglBase.createPbufferSurface(width, height); 186 187 // Create SurfaceTextureHelper and listener. 188 final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create( 189 "SurfaceTextureHelper test" /* threadName */, eglBase.getEglBaseContext()); 190 final MockTextureListener listener = new MockTextureListener(); 191 surfaceTextureHelper.startListening(listener); 192 surfaceTextureHelper.setTextureSize(width, height); 193 194 // Create resources for stubbing an OES texture producer. |eglOesBase| has the SurfaceTexture in 195 // |surfaceTextureHelper| as the target EGLSurface. 196 final EglBase eglOesBase = EglBase.create(eglBase.getEglBaseContext(), EglBase.CONFIG_PLAIN); 197 eglOesBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 198 assertEquals(eglOesBase.surfaceWidth(), width); 199 assertEquals(eglOesBase.surfaceHeight(), height); 200 201 final int red = 79; 202 final int green = 66; 203 final int blue = 161; 204 // Draw a constant color frame onto the SurfaceTexture. 205 eglOesBase.makeCurrent(); 206 GLES20.glClearColor(red / 255.0f, green / 255.0f, blue / 255.0f, 1.0f); 207 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 208 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 209 eglOesBase.swapBuffers(); 210 eglOesBase.release(); 211 212 // Wait for OES texture frame. 213 final VideoFrame.TextureBuffer textureBuffer = listener.waitForTextureBuffer(); 214 // Diconnect while holding the frame. 215 surfaceTextureHelper.dispose(); 216 217 // Draw the pending texture frame onto the pixel buffer. 218 eglBase.makeCurrent(); 219 final GlRectDrawer drawer = new GlRectDrawer(); 220 drawer.drawOes(textureBuffer.getTextureId(), 221 RendererCommon.convertMatrixFromAndroidGraphicsMatrix(textureBuffer.getTransformMatrix()), 222 width, height, 0, 0, width, height); 223 drawer.release(); 224 225 // Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g. Nexus 9. 226 final ByteBuffer rgbaData = ByteBuffer.allocateDirect(width * height * 4); 227 GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaData); 228 GlUtil.checkNoGLES2Error("glReadPixels"); 229 eglBase.release(); 230 231 // Assert rendered image is expected constant color. 232 while (rgbaData.hasRemaining()) { 233 assertEquals(rgbaData.get() & 0xFF, red); 234 assertEquals(rgbaData.get() & 0xFF, green); 235 assertEquals(rgbaData.get() & 0xFF, blue); 236 assertEquals(rgbaData.get() & 0xFF, 255); 237 } 238 // Late frame return after everything has been disposed and released. 239 textureBuffer.release(); 240 } 241 242 /** 243 * Test disposing the SurfaceTextureHelper, but keep trying to produce more texture frames. No 244 * frames should be delivered to the listener. 245 */ 246 @Test 247 @MediumTest testDispose()248 public void testDispose() throws InterruptedException { 249 // Create SurfaceTextureHelper and listener. 250 final SurfaceTextureHelper surfaceTextureHelper = 251 SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, null); 252 final MockTextureListener listener = new MockTextureListener(); 253 surfaceTextureHelper.startListening(listener); 254 // Create EglBase with the SurfaceTexture as target EGLSurface. 255 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); 256 surfaceTextureHelper.setTextureSize(/* textureWidth= */ 32, /* textureHeight= */ 32); 257 eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 258 eglBase.makeCurrent(); 259 // Assert no frame has been received yet. 260 listener.assertNoFrameIsDelivered(/* waitPeriodMs= */ 1); 261 // Draw and wait for one frame. 262 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 263 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 264 eglBase.swapBuffers(); 265 listener.waitForTextureBuffer().release(); 266 267 // Dispose - we should not receive any textures after this. 268 surfaceTextureHelper.dispose(); 269 270 // Draw one frame. 271 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 272 eglBase.swapBuffers(); 273 // swapBuffers() should not trigger onTextureFrameAvailable() because disposed has been called. 274 // Assert that no OES texture was delivered. 275 listener.assertNoFrameIsDelivered(/* waitPeriodMs= */ 500); 276 277 eglBase.release(); 278 } 279 280 /** 281 * Test disposing the SurfaceTextureHelper immediately after is has been setup to use a 282 * shared context. No frames should be delivered to the listener. 283 */ 284 @Test 285 @SmallTest testDisposeImmediately()286 public void testDisposeImmediately() { 287 final SurfaceTextureHelper surfaceTextureHelper = 288 SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, null); 289 surfaceTextureHelper.dispose(); 290 } 291 292 /** 293 * Call stopListening(), but keep trying to produce more texture frames. No frames should be 294 * delivered to the listener. 295 */ 296 @Test 297 @MediumTest testStopListening()298 public void testStopListening() throws InterruptedException { 299 // Create SurfaceTextureHelper and listener. 300 final SurfaceTextureHelper surfaceTextureHelper = 301 SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, null); 302 surfaceTextureHelper.setTextureSize(/* textureWidth= */ 32, /* textureHeight= */ 32); 303 final MockTextureListener listener = new MockTextureListener(); 304 surfaceTextureHelper.startListening(listener); 305 // Create EglBase with the SurfaceTexture as target EGLSurface. 306 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); 307 eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 308 eglBase.makeCurrent(); 309 // Assert no frame has been received yet. 310 listener.assertNoFrameIsDelivered(/* waitPeriodMs= */ 1); 311 // Draw and wait for one frame. 312 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 313 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 314 eglBase.swapBuffers(); 315 listener.waitForTextureBuffer().release(); 316 317 // Stop listening - we should not receive any textures after this. 318 surfaceTextureHelper.stopListening(); 319 320 // Draw one frame. 321 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 322 eglBase.swapBuffers(); 323 // swapBuffers() should not trigger onTextureFrameAvailable() because disposed has been called. 324 // Assert that no OES texture was delivered. 325 listener.assertNoFrameIsDelivered(/* waitPeriodMs= */ 500); 326 327 surfaceTextureHelper.dispose(); 328 eglBase.release(); 329 } 330 331 /** 332 * Test stopListening() immediately after the SurfaceTextureHelper has been setup. 333 */ 334 @Test 335 @SmallTest testStopListeningImmediately()336 public void testStopListeningImmediately() throws InterruptedException { 337 final SurfaceTextureHelper surfaceTextureHelper = 338 SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, null); 339 final MockTextureListener listener = new MockTextureListener(); 340 surfaceTextureHelper.startListening(listener); 341 surfaceTextureHelper.stopListening(); 342 surfaceTextureHelper.dispose(); 343 } 344 345 /** 346 * Test stopListening() immediately after the SurfaceTextureHelper has been setup on the handler 347 * thread. 348 */ 349 @Test 350 @SmallTest testStopListeningImmediatelyOnHandlerThread()351 public void testStopListeningImmediatelyOnHandlerThread() throws InterruptedException { 352 final SurfaceTextureHelper surfaceTextureHelper = 353 SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, null); 354 final MockTextureListener listener = new MockTextureListener(); 355 356 final CountDownLatch stopListeningBarrier = new CountDownLatch(1); 357 final CountDownLatch stopListeningBarrierDone = new CountDownLatch(1); 358 // Start by posting to the handler thread to keep it occupied. 359 surfaceTextureHelper.getHandler().post(new Runnable() { 360 @Override 361 public void run() { 362 ThreadUtils.awaitUninterruptibly(stopListeningBarrier); 363 surfaceTextureHelper.stopListening(); 364 stopListeningBarrierDone.countDown(); 365 } 366 }); 367 368 // startListening() is asynchronous and will post to the occupied handler thread. 369 surfaceTextureHelper.startListening(listener); 370 // Wait for stopListening() to be called on the handler thread. 371 stopListeningBarrier.countDown(); 372 stopListeningBarrierDone.await(); 373 // Wait until handler thread is idle to try to catch late startListening() call. 374 final CountDownLatch barrier = new CountDownLatch(1); 375 surfaceTextureHelper.getHandler().post(new Runnable() { 376 @Override 377 public void run() { 378 barrier.countDown(); 379 } 380 }); 381 ThreadUtils.awaitUninterruptibly(barrier); 382 // Previous startListening() call should never have taken place and it should be ok to call it 383 // again. 384 surfaceTextureHelper.startListening(listener); 385 386 surfaceTextureHelper.dispose(); 387 } 388 389 /** 390 * Test calling startListening() with a new listener after stopListening() has been called. 391 */ 392 @Test 393 @MediumTest testRestartListeningWithNewListener()394 public void testRestartListeningWithNewListener() throws InterruptedException { 395 // Create SurfaceTextureHelper and listener. 396 final SurfaceTextureHelper surfaceTextureHelper = 397 SurfaceTextureHelper.create("SurfaceTextureHelper test" /* threadName */, null); 398 surfaceTextureHelper.setTextureSize(/* textureWidth= */ 32, /* textureHeight= */ 32); 399 final MockTextureListener listener1 = new MockTextureListener(); 400 surfaceTextureHelper.startListening(listener1); 401 // Create EglBase with the SurfaceTexture as target EGLSurface. 402 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); 403 eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 404 eglBase.makeCurrent(); 405 // Assert no frame has been received yet. 406 listener1.assertNoFrameIsDelivered(/* waitPeriodMs= */ 1); 407 // Draw and wait for one frame. 408 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 409 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 410 eglBase.swapBuffers(); 411 listener1.waitForTextureBuffer().release(); 412 413 // Stop listening - |listener1| should not receive any textures after this. 414 surfaceTextureHelper.stopListening(); 415 416 // Connect different listener. 417 final MockTextureListener listener2 = new MockTextureListener(); 418 surfaceTextureHelper.startListening(listener2); 419 // Assert no frame has been received yet. 420 listener2.assertNoFrameIsDelivered(/* waitPeriodMs= */ 1); 421 422 // Draw one frame. 423 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 424 eglBase.swapBuffers(); 425 426 // Check that |listener2| received the frame, and not |listener1|. 427 listener2.waitForTextureBuffer().release(); 428 listener1.assertNoFrameIsDelivered(/* waitPeriodMs= */ 1); 429 430 surfaceTextureHelper.dispose(); 431 eglBase.release(); 432 } 433 434 @Test 435 @MediumTest testTexturetoYuv()436 public void testTexturetoYuv() throws InterruptedException { 437 final int width = 16; 438 final int height = 16; 439 440 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); 441 442 // Create SurfaceTextureHelper and listener. 443 final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create( 444 "SurfaceTextureHelper test" /* threadName */, eglBase.getEglBaseContext()); 445 final MockTextureListener listener = new MockTextureListener(); 446 surfaceTextureHelper.startListening(listener); 447 surfaceTextureHelper.setTextureSize(width, height); 448 449 // Create resources for stubbing an OES texture producer. |eglBase| has the SurfaceTexture in 450 // |surfaceTextureHelper| as the target EGLSurface. 451 452 eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 453 assertEquals(eglBase.surfaceWidth(), width); 454 assertEquals(eglBase.surfaceHeight(), height); 455 456 final int red[] = new int[] {79, 144, 185}; 457 final int green[] = new int[] {66, 210, 162}; 458 final int blue[] = new int[] {161, 117, 158}; 459 460 final int ref_y[] = new int[] {85, 170, 161}; 461 final int ref_u[] = new int[] {168, 97, 123}; 462 final int ref_v[] = new int[] {127, 106, 138}; 463 464 // Draw three frames. 465 for (int i = 0; i < 3; ++i) { 466 // Draw a constant color frame onto the SurfaceTexture. 467 eglBase.makeCurrent(); 468 GLES20.glClearColor(red[i] / 255.0f, green[i] / 255.0f, blue[i] / 255.0f, 1.0f); 469 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 470 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 471 eglBase.swapBuffers(); 472 473 // Wait for an OES texture to arrive. 474 final VideoFrame.TextureBuffer textureBuffer = listener.waitForTextureBuffer(); 475 final VideoFrame.I420Buffer i420 = textureBuffer.toI420(); 476 textureBuffer.release(); 477 478 // Memory layout: Lines are 16 bytes. First 16 lines are 479 // the Y data. These are followed by 8 lines with 8 bytes of U 480 // data on the left and 8 bytes of V data on the right. 481 // 482 // Offset 483 // 0 YYYYYYYY YYYYYYYY 484 // 16 YYYYYYYY YYYYYYYY 485 // ... 486 // 240 YYYYYYYY YYYYYYYY 487 // 256 UUUUUUUU VVVVVVVV 488 // 272 UUUUUUUU VVVVVVVV 489 // ... 490 // 368 UUUUUUUU VVVVVVVV 491 // 384 buffer end 492 493 // Allow off-by-one differences due to different rounding. 494 final ByteBuffer dataY = i420.getDataY(); 495 final int strideY = i420.getStrideY(); 496 for (int y = 0; y < height; y++) { 497 for (int x = 0; x < width; x++) { 498 assertClose(1, ref_y[i], dataY.get(y * strideY + x) & 0xFF); 499 } 500 } 501 502 final int chromaWidth = width / 2; 503 final int chromaHeight = height / 2; 504 505 final ByteBuffer dataU = i420.getDataU(); 506 final ByteBuffer dataV = i420.getDataV(); 507 final int strideU = i420.getStrideU(); 508 final int strideV = i420.getStrideV(); 509 for (int y = 0; y < chromaHeight; y++) { 510 for (int x = 0; x < chromaWidth; x++) { 511 assertClose(1, ref_u[i], dataU.get(y * strideU + x) & 0xFF); 512 assertClose(1, ref_v[i], dataV.get(y * strideV + x) & 0xFF); 513 } 514 } 515 i420.release(); 516 } 517 518 surfaceTextureHelper.dispose(); 519 eglBase.release(); 520 } 521 } 522