1 /* 2 * libjingle 3 * Copyright 2015 Google Inc. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, 9 * this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright notice, 11 * this list of conditions and the following disclaimer in the documentation 12 * and/or other materials provided with the distribution. 13 * 3. The name of the author may not be used to endorse or promote products 14 * derived from this software without specific prior written permission. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 */ 27 package org.webrtc; 28 29 import android.graphics.SurfaceTexture; 30 import android.opengl.GLES20; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.SystemClock; 34 import android.test.ActivityTestCase; 35 import android.test.suitebuilder.annotation.MediumTest; 36 import android.test.suitebuilder.annotation.SmallTest; 37 38 import java.nio.ByteBuffer; 39 40 public final class SurfaceTextureHelperTest extends ActivityTestCase { 41 /** 42 * Mock texture listener with blocking wait functionality. 43 */ 44 public static final class MockTextureListener 45 implements SurfaceTextureHelper.OnTextureFrameAvailableListener { 46 public int oesTextureId; 47 public float[] transformMatrix; 48 private boolean hasNewFrame = false; 49 // Thread where frames are expected to be received on. 50 private final Thread expectedThread; 51 MockTextureListener()52 MockTextureListener() { 53 this.expectedThread = null; 54 } 55 MockTextureListener(Thread expectedThread)56 MockTextureListener(Thread expectedThread) { 57 this.expectedThread = expectedThread; 58 } 59 60 @Override onTextureFrameAvailable( int oesTextureId, float[] transformMatrix, long timestampNs)61 public synchronized void onTextureFrameAvailable( 62 int oesTextureId, float[] transformMatrix, long timestampNs) { 63 if (expectedThread != null && Thread.currentThread() != expectedThread) { 64 throw new IllegalStateException("onTextureFrameAvailable called on wrong thread."); 65 } 66 this.oesTextureId = oesTextureId; 67 this.transformMatrix = transformMatrix; 68 hasNewFrame = true; 69 notifyAll(); 70 } 71 72 /** 73 * Wait indefinitely for a new frame. 74 */ waitForNewFrame()75 public synchronized void waitForNewFrame() throws InterruptedException { 76 while (!hasNewFrame) { 77 wait(); 78 } 79 hasNewFrame = false; 80 } 81 82 /** 83 * Wait for a new frame, or until the specified timeout elapses. Returns true if a new frame was 84 * received before the timeout. 85 */ waitForNewFrame(final long timeoutMs)86 public synchronized boolean waitForNewFrame(final long timeoutMs) throws InterruptedException { 87 final long startTimeMs = SystemClock.elapsedRealtime(); 88 long timeRemainingMs = timeoutMs; 89 while (!hasNewFrame && timeRemainingMs > 0) { 90 wait(timeRemainingMs); 91 final long elapsedTimeMs = SystemClock.elapsedRealtime() - startTimeMs; 92 timeRemainingMs = timeoutMs - elapsedTimeMs; 93 } 94 final boolean didReceiveFrame = hasNewFrame; 95 hasNewFrame = false; 96 return didReceiveFrame; 97 } 98 } 99 100 /** Assert that two integers are close, with difference at most 101 * {@code threshold}. */ assertClose(int threshold, int expected, int actual)102 public static void assertClose(int threshold, int expected, int actual) { 103 if (Math.abs(expected - actual) <= threshold) 104 return; 105 failNotEquals("Not close enough, threshold " + threshold, expected, actual); 106 } 107 108 /** 109 * Test normal use by receiving three uniform texture frames. Texture frames are returned as early 110 * as possible. The texture pixel values are inspected by drawing the texture frame to a pixel 111 * buffer and reading it back with glReadPixels(). 112 */ 113 @MediumTest testThreeConstantColorFrames()114 public static void testThreeConstantColorFrames() throws InterruptedException { 115 final int width = 16; 116 final int height = 16; 117 // Create EGL base with a pixel buffer as display output. 118 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER); 119 eglBase.createPbufferSurface(width, height); 120 final GlRectDrawer drawer = new GlRectDrawer(); 121 122 // Create SurfaceTextureHelper and listener. 123 final SurfaceTextureHelper surfaceTextureHelper = 124 SurfaceTextureHelper.create(eglBase.getEglBaseContext()); 125 final MockTextureListener listener = new MockTextureListener(); 126 surfaceTextureHelper.setListener(listener); 127 surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height); 128 129 // Create resources for stubbing an OES texture producer. |eglOesBase| has the SurfaceTexture in 130 // |surfaceTextureHelper| as the target EGLSurface. 131 final EglBase eglOesBase = 132 EglBase.create(eglBase.getEglBaseContext(), EglBase.CONFIG_PLAIN); 133 eglOesBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 134 assertEquals(eglOesBase.surfaceWidth(), width); 135 assertEquals(eglOesBase.surfaceHeight(), height); 136 137 final int red[] = new int[] {79, 144, 185}; 138 final int green[] = new int[] {66, 210, 162}; 139 final int blue[] = new int[] {161, 117, 158}; 140 // Draw three frames. 141 for (int i = 0; i < 3; ++i) { 142 // Draw a constant color frame onto the SurfaceTexture. 143 eglOesBase.makeCurrent(); 144 GLES20.glClearColor(red[i] / 255.0f, green[i] / 255.0f, blue[i] / 255.0f, 1.0f); 145 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 146 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 147 eglOesBase.swapBuffers(); 148 149 // Wait for an OES texture to arrive and draw it onto the pixel buffer. 150 listener.waitForNewFrame(); 151 eglBase.makeCurrent(); 152 drawer.drawOes(listener.oesTextureId, listener.transformMatrix, 0, 0, width, height); 153 154 surfaceTextureHelper.returnTextureFrame(); 155 156 // Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g. 157 // Nexus 9. 158 final ByteBuffer rgbaData = ByteBuffer.allocateDirect(width * height * 4); 159 GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaData); 160 GlUtil.checkNoGLES2Error("glReadPixels"); 161 162 // Assert rendered image is expected constant color. 163 while (rgbaData.hasRemaining()) { 164 assertEquals(rgbaData.get() & 0xFF, red[i]); 165 assertEquals(rgbaData.get() & 0xFF, green[i]); 166 assertEquals(rgbaData.get() & 0xFF, blue[i]); 167 assertEquals(rgbaData.get() & 0xFF, 255); 168 } 169 } 170 171 drawer.release(); 172 surfaceTextureHelper.disconnect(); 173 eglBase.release(); 174 } 175 176 /** 177 * Test disconnecting the SurfaceTextureHelper while holding a pending texture frame. The pending 178 * texture frame should still be valid, and this is tested by drawing the texture frame to a pixel 179 * buffer and reading it back with glReadPixels(). 180 */ 181 @MediumTest testLateReturnFrame()182 public static void testLateReturnFrame() throws InterruptedException { 183 final int width = 16; 184 final int height = 16; 185 // Create EGL base with a pixel buffer as display output. 186 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PIXEL_BUFFER); 187 eglBase.createPbufferSurface(width, height); 188 189 // Create SurfaceTextureHelper and listener. 190 final SurfaceTextureHelper surfaceTextureHelper = 191 SurfaceTextureHelper.create(eglBase.getEglBaseContext()); 192 final MockTextureListener listener = new MockTextureListener(); 193 surfaceTextureHelper.setListener(listener); 194 surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height); 195 196 // Create resources for stubbing an OES texture producer. |eglOesBase| has the SurfaceTexture in 197 // |surfaceTextureHelper| as the target EGLSurface. 198 final EglBase eglOesBase = 199 EglBase.create(eglBase.getEglBaseContext(), EglBase.CONFIG_PLAIN); 200 eglOesBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 201 assertEquals(eglOesBase.surfaceWidth(), width); 202 assertEquals(eglOesBase.surfaceHeight(), height); 203 204 final int red = 79; 205 final int green = 66; 206 final int blue = 161; 207 // Draw a constant color frame onto the SurfaceTexture. 208 eglOesBase.makeCurrent(); 209 GLES20.glClearColor(red / 255.0f, green / 255.0f, blue / 255.0f, 1.0f); 210 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 211 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 212 eglOesBase.swapBuffers(); 213 eglOesBase.release(); 214 215 // Wait for OES texture frame. 216 listener.waitForNewFrame(); 217 // Diconnect while holding the frame. 218 surfaceTextureHelper.disconnect(); 219 220 // Draw the pending texture frame onto the pixel buffer. 221 eglBase.makeCurrent(); 222 final GlRectDrawer drawer = new GlRectDrawer(); 223 drawer.drawOes(listener.oesTextureId, listener.transformMatrix, 0, 0, width, height); 224 drawer.release(); 225 226 // Download the pixels in the pixel buffer as RGBA. Not all platforms support RGB, e.g. Nexus 9. 227 final ByteBuffer rgbaData = ByteBuffer.allocateDirect(width * height * 4); 228 GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaData); 229 GlUtil.checkNoGLES2Error("glReadPixels"); 230 eglBase.release(); 231 232 // Assert rendered image is expected constant color. 233 while (rgbaData.hasRemaining()) { 234 assertEquals(rgbaData.get() & 0xFF, red); 235 assertEquals(rgbaData.get() & 0xFF, green); 236 assertEquals(rgbaData.get() & 0xFF, blue); 237 assertEquals(rgbaData.get() & 0xFF, 255); 238 } 239 // Late frame return after everything has been disconnected and released. 240 surfaceTextureHelper.returnTextureFrame(); 241 } 242 243 /** 244 * Test disconnecting the SurfaceTextureHelper, but keep trying to produce more texture frames. No 245 * frames should be delivered to the listener. 246 */ 247 @MediumTest testDisconnect()248 public static void testDisconnect() throws InterruptedException { 249 // Create SurfaceTextureHelper and listener. 250 final SurfaceTextureHelper surfaceTextureHelper = 251 SurfaceTextureHelper.create(null); 252 final MockTextureListener listener = new MockTextureListener(); 253 surfaceTextureHelper.setListener(listener); 254 // Create EglBase with the SurfaceTexture as target EGLSurface. 255 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); 256 eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 257 eglBase.makeCurrent(); 258 // Assert no frame has been received yet. 259 assertFalse(listener.waitForNewFrame(1)); 260 // Draw and wait for one frame. 261 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 262 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 263 eglBase.swapBuffers(); 264 listener.waitForNewFrame(); 265 surfaceTextureHelper.returnTextureFrame(); 266 267 // Disconnect - we should not receive any textures after this. 268 surfaceTextureHelper.disconnect(); 269 270 // Draw one frame. 271 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 272 eglBase.swapBuffers(); 273 // swapBuffers() should not trigger onTextureFrameAvailable() because we are disconnected. 274 // Assert that no OES texture was delivered. 275 assertFalse(listener.waitForNewFrame(500)); 276 277 eglBase.release(); 278 } 279 280 /** 281 * Test disconnecting the SurfaceTextureHelper immediately after is has been setup to use a 282 * shared context. No frames should be delivered to the listener. 283 */ 284 @SmallTest testDisconnectImmediately()285 public static void testDisconnectImmediately() { 286 final SurfaceTextureHelper surfaceTextureHelper = 287 SurfaceTextureHelper.create(null); 288 surfaceTextureHelper.disconnect(); 289 } 290 291 /** 292 * Test use SurfaceTextureHelper on a separate thread. A uniform texture frame is created and 293 * received on a thread separate from the test thread. 294 */ 295 @MediumTest testFrameOnSeparateThread()296 public static void testFrameOnSeparateThread() throws InterruptedException { 297 final HandlerThread thread = new HandlerThread("SurfaceTextureHelperTestThread"); 298 thread.start(); 299 final Handler handler = new Handler(thread.getLooper()); 300 301 // Create SurfaceTextureHelper and listener. 302 final SurfaceTextureHelper surfaceTextureHelper = 303 SurfaceTextureHelper.create(null, handler); 304 // Create a mock listener and expect frames to be delivered on |thread|. 305 final MockTextureListener listener = new MockTextureListener(thread); 306 surfaceTextureHelper.setListener(listener); 307 308 // Create resources for stubbing an OES texture producer. |eglOesBase| has the 309 // SurfaceTexture in |surfaceTextureHelper| as the target EGLSurface. 310 final EglBase eglOesBase = EglBase.create(null, EglBase.CONFIG_PLAIN); 311 eglOesBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 312 eglOesBase.makeCurrent(); 313 // Draw a frame onto the SurfaceTexture. 314 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 315 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 316 eglOesBase.swapBuffers(); 317 eglOesBase.release(); 318 319 // Wait for an OES texture to arrive. 320 listener.waitForNewFrame(); 321 322 // Return the frame from this thread. 323 surfaceTextureHelper.returnTextureFrame(); 324 surfaceTextureHelper.disconnect(handler); 325 } 326 327 /** 328 * Test use SurfaceTextureHelper on a separate thread. A uniform texture frame is created and 329 * received on a thread separate from the test thread and returned after disconnect. 330 */ 331 @MediumTest testLateReturnFrameOnSeparateThread()332 public static void testLateReturnFrameOnSeparateThread() throws InterruptedException { 333 final HandlerThread thread = new HandlerThread("SurfaceTextureHelperTestThread"); 334 thread.start(); 335 final Handler handler = new Handler(thread.getLooper()); 336 337 // Create SurfaceTextureHelper and listener. 338 final SurfaceTextureHelper surfaceTextureHelper = 339 SurfaceTextureHelper.create(null, handler); 340 // Create a mock listener and expect frames to be delivered on |thread|. 341 final MockTextureListener listener = new MockTextureListener(thread); 342 surfaceTextureHelper.setListener(listener); 343 344 // Create resources for stubbing an OES texture producer. |eglOesBase| has the 345 // SurfaceTexture in |surfaceTextureHelper| as the target EGLSurface. 346 final EglBase eglOesBase = EglBase.create(null, EglBase.CONFIG_PLAIN); 347 eglOesBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 348 eglOesBase.makeCurrent(); 349 // Draw a frame onto the SurfaceTexture. 350 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 351 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 352 eglOesBase.swapBuffers(); 353 eglOesBase.release(); 354 355 // Wait for an OES texture to arrive. 356 listener.waitForNewFrame(); 357 358 surfaceTextureHelper.disconnect(handler); 359 360 surfaceTextureHelper.returnTextureFrame(); 361 } 362 363 @MediumTest testTexturetoYUV()364 public static void testTexturetoYUV() throws InterruptedException { 365 final int width = 16; 366 final int height = 16; 367 368 final EglBase eglBase = EglBase.create(null, EglBase.CONFIG_PLAIN); 369 370 // Create SurfaceTextureHelper and listener. 371 final SurfaceTextureHelper surfaceTextureHelper = 372 SurfaceTextureHelper.create(eglBase.getEglBaseContext()); 373 final MockTextureListener listener = new MockTextureListener(); 374 surfaceTextureHelper.setListener(listener); 375 surfaceTextureHelper.getSurfaceTexture().setDefaultBufferSize(width, height); 376 377 // Create resources for stubbing an OES texture producer. |eglBase| has the SurfaceTexture in 378 // |surfaceTextureHelper| as the target EGLSurface. 379 380 eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture()); 381 assertEquals(eglBase.surfaceWidth(), width); 382 assertEquals(eglBase.surfaceHeight(), height); 383 384 final int red[] = new int[] {79, 144, 185}; 385 final int green[] = new int[] {66, 210, 162}; 386 final int blue[] = new int[] {161, 117, 158}; 387 388 final int ref_y[] = new int[] {81, 180, 168}; 389 final int ref_u[] = new int[] {173, 93, 122}; 390 final int ref_v[] = new int[] {127, 103, 140}; 391 392 // Draw three frames. 393 for (int i = 0; i < 3; ++i) { 394 // Draw a constant color frame onto the SurfaceTexture. 395 eglBase.makeCurrent(); 396 GLES20.glClearColor(red[i] / 255.0f, green[i] / 255.0f, blue[i] / 255.0f, 1.0f); 397 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 398 // swapBuffers() will ultimately trigger onTextureFrameAvailable(). 399 eglBase.swapBuffers(); 400 401 // Wait for an OES texture to arrive. 402 listener.waitForNewFrame(); 403 404 // Memory layout: Lines are 16 bytes. First 16 lines are 405 // the Y data. These are followed by 8 lines with 8 bytes of U 406 // data on the left and 8 bytes of V data on the right. 407 // 408 // Offset 409 // 0 YYYYYYYY YYYYYYYY 410 // 16 YYYYYYYY YYYYYYYY 411 // ... 412 // 240 YYYYYYYY YYYYYYYY 413 // 256 UUUUUUUU VVVVVVVV 414 // 272 UUUUUUUU VVVVVVVV 415 // ... 416 // 368 UUUUUUUU VVVVVVVV 417 // 384 buffer end 418 ByteBuffer buffer = ByteBuffer.allocateDirect(width * height * 3 / 2); 419 surfaceTextureHelper.textureToYUV(buffer, width, height, width, 420 listener.oesTextureId, listener.transformMatrix); 421 422 surfaceTextureHelper.returnTextureFrame(); 423 424 // Allow off-by-one differences due to different rounding. 425 while (buffer.position() < width*height) { 426 assertClose(1, buffer.get() & 0xff, ref_y[i]); 427 } 428 while (buffer.hasRemaining()) { 429 if (buffer.position() % width < width/2) 430 assertClose(1, buffer.get() & 0xff, ref_u[i]); 431 else 432 assertClose(1, buffer.get() & 0xff, ref_v[i]); 433 } 434 } 435 436 surfaceTextureHelper.disconnect(); 437 eglBase.release(); 438 } 439 } 440