• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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