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 */
27package org.webrtc;
28
29import android.graphics.SurfaceTexture;
30import android.opengl.GLES20;
31import android.os.Handler;
32import android.os.HandlerThread;
33import android.os.SystemClock;
34import android.test.ActivityTestCase;
35import android.test.suitebuilder.annotation.MediumTest;
36import android.test.suitebuilder.annotation.SmallTest;
37
38import java.nio.ByteBuffer;
39
40public 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
52    MockTextureListener() {
53      this.expectedThread = null;
54    }
55
56    MockTextureListener(Thread expectedThread) {
57      this.expectedThread = expectedThread;
58    }
59
60    @Override
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     */
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     */
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}. */
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
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
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
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
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
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
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
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