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