• 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 
28 package org.webrtc;
29 
30 import android.graphics.SurfaceTexture;
31 import android.opengl.GLES11Ext;
32 import android.opengl.GLES20;
33 import android.os.Build;
34 import android.os.Handler;
35 import android.os.HandlerThread;
36 import android.os.SystemClock;
37 
38 import java.nio.ByteBuffer;
39 import java.nio.FloatBuffer;
40 import java.util.concurrent.Callable;
41 import java.util.concurrent.CountDownLatch;
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * Helper class to create and synchronize access to a SurfaceTexture. The caller will get notified
46  * of new frames in onTextureFrameAvailable(), and should call returnTextureFrame() when done with
47  * the frame. Only one texture frame can be in flight at once, so returnTextureFrame() must be
48  * called in order to receive a new frame. Call disconnect() to stop receiveing new frames and
49  * release all resources.
50  * Note that there is a C++ counter part of this class that optionally can be used. It is used for
51  * wrapping texture frames into webrtc::VideoFrames and also handles calling returnTextureFrame()
52  * when the webrtc::VideoFrame is no longer used.
53  */
54 class SurfaceTextureHelper {
55   private static final String TAG = "SurfaceTextureHelper";
56   /**
57    * Callback interface for being notified that a new texture frame is available. The calls will be
58    * made on a dedicated thread with a bound EGLContext. The thread will be the same throughout the
59    * lifetime of the SurfaceTextureHelper instance, but different from the thread calling the
60    * SurfaceTextureHelper constructor. The callee is not allowed to make another EGLContext current
61    * on the calling thread.
62    */
63   public interface OnTextureFrameAvailableListener {
onTextureFrameAvailable( int oesTextureId, float[] transformMatrix, long timestampNs)64     abstract void onTextureFrameAvailable(
65         int oesTextureId, float[] transformMatrix, long timestampNs);
66   }
67 
create(EglBase.Context sharedContext)68   public static SurfaceTextureHelper create(EglBase.Context sharedContext) {
69     return create(sharedContext, null);
70   }
71 
72   /**
73    * Construct a new SurfaceTextureHelper sharing OpenGL resources with |sharedContext|. If
74    * |handler| is non-null, the callback will be executed on that handler's thread. If |handler| is
75    * null, a dedicated private thread is created for the callbacks.
76    */
create(final EglBase.Context sharedContext, final Handler handler)77   public static SurfaceTextureHelper create(final EglBase.Context sharedContext,
78       final Handler handler) {
79     final Handler finalHandler;
80     if (handler != null) {
81       finalHandler = handler;
82     } else {
83       final HandlerThread thread = new HandlerThread(TAG);
84       thread.start();
85       finalHandler = new Handler(thread.getLooper());
86     }
87     // The onFrameAvailable() callback will be executed on the SurfaceTexture ctor thread. See:
88     // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/graphics/SurfaceTexture.java#195.
89     // Therefore, in order to control the callback thread on API lvl < 21, the SurfaceTextureHelper
90     // is constructed on the |handler| thread.
91     return ThreadUtils.invokeUninterruptibly(finalHandler, new Callable<SurfaceTextureHelper>() {
92       @Override public SurfaceTextureHelper call() {
93         return new SurfaceTextureHelper(sharedContext, finalHandler, (handler == null));
94       }
95     });
96   }
97 
98   // State for YUV conversion, instantiated on demand.
99   static private class YuvConverter {
100     private final EglBase eglBase;
101     private final GlShader shader;
102     private boolean released = false;
103 
104     // Vertex coordinates in Normalized Device Coordinates, i.e.
105     // (-1, -1) is bottom-left and (1, 1) is top-right.
106     private static final FloatBuffer DEVICE_RECTANGLE =
107         GlUtil.createFloatBuffer(new float[] {
108               -1.0f, -1.0f,  // Bottom left.
109                1.0f, -1.0f,  // Bottom right.
110               -1.0f,  1.0f,  // Top left.
111                1.0f,  1.0f,  // Top right.
112             });
113 
114     // Texture coordinates - (0, 0) is bottom-left and (1, 1) is top-right.
115     private static final FloatBuffer TEXTURE_RECTANGLE =
116         GlUtil.createFloatBuffer(new float[] {
117               0.0f, 0.0f,  // Bottom left.
118               1.0f, 0.0f,  // Bottom right.
119               0.0f, 1.0f,  // Top left.
120               1.0f, 1.0f   // Top right.
121             });
122 
123     private static final String VERTEX_SHADER =
124         "varying vec2 interp_tc;\n"
125       + "attribute vec4 in_pos;\n"
126       + "attribute vec4 in_tc;\n"
127       + "\n"
128       + "uniform mat4 texMatrix;\n"
129       + "\n"
130       + "void main() {\n"
131       + "    gl_Position = in_pos;\n"
132       + "    interp_tc = (texMatrix * in_tc).xy;\n"
133       + "}\n";
134 
135     private static final String FRAGMENT_SHADER =
136         "#extension GL_OES_EGL_image_external : require\n"
137       + "precision mediump float;\n"
138       + "varying vec2 interp_tc;\n"
139       + "\n"
140       + "uniform samplerExternalOES oesTex;\n"
141       // Difference in texture coordinate corresponding to one
142       // sub-pixel in the x direction.
143       + "uniform vec2 xUnit;\n"
144       // Color conversion coefficients, including constant term
145       + "uniform vec4 coeffs;\n"
146       + "\n"
147       + "void main() {\n"
148       // Since the alpha read from the texture is always 1, this could
149       // be written as a mat4 x vec4 multiply. However, that seems to
150       // give a worse framerate, possibly because the additional
151       // multiplies by 1.0 consume resources. TODO(nisse): Could also
152       // try to do it as a vec3 x mat3x4, followed by an add in of a
153       // constant vector.
154       + "  gl_FragColor.r = coeffs.a + dot(coeffs.rgb,\n"
155       + "      texture2D(oesTex, interp_tc - 1.5 * xUnit).rgb);\n"
156       + "  gl_FragColor.g = coeffs.a + dot(coeffs.rgb,\n"
157       + "      texture2D(oesTex, interp_tc - 0.5 * xUnit).rgb);\n"
158       + "  gl_FragColor.b = coeffs.a + dot(coeffs.rgb,\n"
159       + "      texture2D(oesTex, interp_tc + 0.5 * xUnit).rgb);\n"
160       + "  gl_FragColor.a = coeffs.a + dot(coeffs.rgb,\n"
161       + "      texture2D(oesTex, interp_tc + 1.5 * xUnit).rgb);\n"
162       + "}\n";
163 
164     private int texMatrixLoc;
165     private int xUnitLoc;
166     private int coeffsLoc;;
167 
168     YuvConverter (EglBase.Context sharedContext) {
169       eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_RGBA_BUFFER);
170       eglBase.createDummyPbufferSurface();
171       eglBase.makeCurrent();
172 
173       shader = new GlShader(VERTEX_SHADER, FRAGMENT_SHADER);
174       shader.useProgram();
175       texMatrixLoc = shader.getUniformLocation("texMatrix");
176       xUnitLoc = shader.getUniformLocation("xUnit");
177       coeffsLoc = shader.getUniformLocation("coeffs");
178       GLES20.glUniform1i(shader.getUniformLocation("oesTex"), 0);
179       GlUtil.checkNoGLES2Error("Initialize fragment shader uniform values.");
180       // Initialize vertex shader attributes.
181       shader.setVertexAttribArray("in_pos", 2, DEVICE_RECTANGLE);
182       // If the width is not a multiple of 4 pixels, the texture
183       // will be scaled up slightly and clipped at the right border.
184       shader.setVertexAttribArray("in_tc", 2, TEXTURE_RECTANGLE);
185       eglBase.detachCurrent();
186     }
187 
188     synchronized void convert(ByteBuffer buf,
189         int width, int height, int stride, int textureId, float [] transformMatrix) {
190       if (released) {
191         throw new IllegalStateException(
192             "YuvConverter.convert called on released object");
193       }
194 
195       // We draw into a buffer laid out like
196       //
197       //    +---------+
198       //    |         |
199       //    |  Y      |
200       //    |         |
201       //    |         |
202       //    +----+----+
203       //    | U  | V  |
204       //    |    |    |
205       //    +----+----+
206       //
207       // In memory, we use the same stride for all of Y, U and V. The
208       // U data starts at offset |height| * |stride| from the Y data,
209       // and the V data starts at at offset |stride/2| from the U
210       // data, with rows of U and V data alternating.
211       //
212       // Now, it would have made sense to allocate a pixel buffer with
213       // a single byte per pixel (EGL10.EGL_COLOR_BUFFER_TYPE,
214       // EGL10.EGL_LUMINANCE_BUFFER,), but that seems to be
215       // unsupported by devices. So do the following hack: Allocate an
216       // RGBA buffer, of width |stride|/4. To render each of these
217       // large pixels, sample the texture at 4 different x coordinates
218       // and store the results in the four components.
219       //
220       // Since the V data needs to start on a boundary of such a
221       // larger pixel, it is not sufficient that |stride| is even, it
222       // has to be a multiple of 8 pixels.
223 
224       if (stride % 8 != 0) {
225         throw new IllegalArgumentException(
226             "Invalid stride, must be a multiple of 8");
227       }
228       if (stride < width){
229         throw new IllegalArgumentException(
230             "Invalid stride, must >= width");
231       }
232 
233       int y_width = (width+3) / 4;
234       int uv_width = (width+7) / 8;
235       int uv_height = (height+1)/2;
236       int total_height = height + uv_height;
237       int size = stride * total_height;
238 
239       if (buf.capacity() < size) {
240         throw new IllegalArgumentException("YuvConverter.convert called with too small buffer");
241       }
242       // Produce a frame buffer starting at top-left corner, not
243       // bottom-left.
244       transformMatrix =
245           RendererCommon.multiplyMatrices(transformMatrix,
246               RendererCommon.verticalFlipMatrix());
247 
248       // Create new pBuffferSurface with the correct size if needed.
249       if (eglBase.hasSurface()) {
250         if (eglBase.surfaceWidth() != stride/4 ||
251             eglBase.surfaceHeight() != total_height){
252           eglBase.releaseSurface();
253           eglBase.createPbufferSurface(stride/4, total_height);
254         }
255       } else {
256         eglBase.createPbufferSurface(stride/4, total_height);
257       }
258 
259       eglBase.makeCurrent();
260 
261       GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
262       GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
263       GLES20.glUniformMatrix4fv(texMatrixLoc, 1, false, transformMatrix, 0);
264 
265       // Draw Y
266       GLES20.glViewport(0, 0, y_width, height);
267       // Matrix * (1;0;0;0) / width. Note that opengl uses column major order.
268       GLES20.glUniform2f(xUnitLoc,
269           transformMatrix[0] / width,
270           transformMatrix[1] / width);
271       // Y'UV444 to RGB888, see
272       // https://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion.
273       // We use the ITU-R coefficients for U and V */
274       GLES20.glUniform4f(coeffsLoc, 0.299f, 0.587f, 0.114f, 0.0f);
275       GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
276 
277       // Draw U
278       GLES20.glViewport(0, height, uv_width, uv_height);
279       // Matrix * (1;0;0;0) / (2*width). Note that opengl uses column major order.
280       GLES20.glUniform2f(xUnitLoc,
281           transformMatrix[0] / (2.0f*width),
282           transformMatrix[1] / (2.0f*width));
283       GLES20.glUniform4f(coeffsLoc, -0.169f, -0.331f, 0.499f, 0.5f);
284       GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
285 
286       // Draw V
287       GLES20.glViewport(stride/8, height, uv_width, uv_height);
288       GLES20.glUniform4f(coeffsLoc, 0.499f, -0.418f, -0.0813f, 0.5f);
289       GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
290 
291       GLES20.glReadPixels(0, 0, stride/4, total_height, GLES20.GL_RGBA,
292           GLES20.GL_UNSIGNED_BYTE, buf);
293 
294       GlUtil.checkNoGLES2Error("YuvConverter.convert");
295 
296       // Unbind texture. Reportedly needed on some devices to get
297       // the texture updated from the camera.
298       GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
299       eglBase.detachCurrent();
300     }
301 
302     synchronized void release() {
303       released = true;
304       eglBase.makeCurrent();
305       shader.release();
306       eglBase.release();
307     }
308   }
309 
310   private final Handler handler;
311   private boolean isOwningThread;
312   private final EglBase eglBase;
313   private final SurfaceTexture surfaceTexture;
314   private final int oesTextureId;
315   private YuvConverter yuvConverter;
316 
317   private OnTextureFrameAvailableListener listener;
318   // The possible states of this class.
319   private boolean hasPendingTexture = false;
320   private volatile boolean isTextureInUse = false;
321   private boolean isQuitting = false;
322 
323   private SurfaceTextureHelper(EglBase.Context sharedContext,
324       Handler handler, boolean isOwningThread) {
325     if (handler.getLooper().getThread() != Thread.currentThread()) {
326       throw new IllegalStateException("SurfaceTextureHelper must be created on the handler thread");
327     }
328     this.handler = handler;
329     this.isOwningThread = isOwningThread;
330 
331     eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
332     eglBase.createDummyPbufferSurface();
333     eglBase.makeCurrent();
334 
335     oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
336     surfaceTexture = new SurfaceTexture(oesTextureId);
337   }
338 
339   private YuvConverter getYuvConverter() {
340     // yuvConverter is assigned once
341     if (yuvConverter != null)
342       return yuvConverter;
343 
344     synchronized(this) {
345       if (yuvConverter == null)
346         yuvConverter = new YuvConverter(eglBase.getEglBaseContext());
347       return yuvConverter;
348     }
349   }
350 
351   /**
352    *  Start to stream textures to the given |listener|.
353    *  A Listener can only be set once.
354    */
355   public void setListener(OnTextureFrameAvailableListener listener) {
356     if (this.listener != null) {
357       throw new IllegalStateException("SurfaceTextureHelper listener has already been set.");
358     }
359     this.listener = listener;
360     surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
361       @Override
362       public void onFrameAvailable(SurfaceTexture surfaceTexture) {
363         hasPendingTexture = true;
364         tryDeliverTextureFrame();
365       }
366     });
367   }
368 
369   /**
370    * Retrieve the underlying SurfaceTexture. The SurfaceTexture should be passed in to a video
371    * producer such as a camera or decoder.
372    */
373   public SurfaceTexture getSurfaceTexture() {
374     return surfaceTexture;
375   }
376 
377   /**
378    * Call this function to signal that you are done with the frame received in
379    * onTextureFrameAvailable(). Only one texture frame can be in flight at once, so you must call
380    * this function in order to receive a new frame.
381    */
382   public void returnTextureFrame() {
383     handler.post(new Runnable() {
384       @Override public void run() {
385         isTextureInUse = false;
386         if (isQuitting) {
387           release();
388         } else {
389           tryDeliverTextureFrame();
390         }
391       }
392     });
393   }
394 
395   public boolean isTextureInUse() {
396     return isTextureInUse;
397   }
398 
399   /**
400    * Call disconnect() to stop receiving frames. Resources are released when the texture frame has
401    * been returned by a call to returnTextureFrame(). You are guaranteed to not receive any more
402    * onTextureFrameAvailable() after this function returns.
403    */
404   public void disconnect() {
405     if (!isOwningThread) {
406       throw new IllegalStateException("Must call disconnect(handler).");
407     }
408     if (handler.getLooper().getThread() == Thread.currentThread()) {
409       isQuitting = true;
410       if (!isTextureInUse) {
411         release();
412       }
413       return;
414     }
415     final CountDownLatch barrier = new CountDownLatch(1);
416     handler.postAtFrontOfQueue(new Runnable() {
417       @Override public void run() {
418         isQuitting = true;
419         barrier.countDown();
420         if (!isTextureInUse) {
421           release();
422         }
423       }
424     });
425     ThreadUtils.awaitUninterruptibly(barrier);
426   }
427 
428   /**
429    * Call disconnect() to stop receiving frames and quit the looper used by |handler|.
430    * Resources are released when the texture frame has been returned by a call to
431    * returnTextureFrame(). You are guaranteed to not receive any more
432    * onTextureFrameAvailable() after this function returns.
433    */
434   public void disconnect(Handler handler) {
435     if (this.handler != handler) {
436       throw new IllegalStateException("Wrong handler.");
437     }
438     isOwningThread = true;
439     disconnect();
440   }
441 
442   public void textureToYUV(ByteBuffer buf,
443       int width, int height, int stride, int textureId, float [] transformMatrix) {
444     if (textureId != oesTextureId)
445       throw new IllegalStateException("textureToByteBuffer called with unexpected textureId");
446 
447     getYuvConverter().convert(buf, width, height, stride, textureId, transformMatrix);
448   }
449 
450   private void tryDeliverTextureFrame() {
451     if (handler.getLooper().getThread() != Thread.currentThread()) {
452       throw new IllegalStateException("Wrong thread.");
453     }
454     if (isQuitting || !hasPendingTexture || isTextureInUse) {
455       return;
456     }
457     isTextureInUse = true;
458     hasPendingTexture = false;
459 
460     eglBase.makeCurrent();
461     surfaceTexture.updateTexImage();
462 
463     final float[] transformMatrix = new float[16];
464     surfaceTexture.getTransformMatrix(transformMatrix);
465     final long timestampNs = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
466         ? surfaceTexture.getTimestamp()
467         : TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime());
468     listener.onTextureFrameAvailable(oesTextureId, transformMatrix, timestampNs);
469   }
470 
471   private void release() {
472     if (handler.getLooper().getThread() != Thread.currentThread()) {
473       throw new IllegalStateException("Wrong thread.");
474     }
475     if (isTextureInUse || !isQuitting) {
476       throw new IllegalStateException("Unexpected release.");
477     }
478     synchronized (this) {
479       if (yuvConverter != null)
480         yuvConverter.release();
481     }
482     eglBase.makeCurrent();
483     GLES20.glDeleteTextures(1, new int[] {oesTextureId}, 0);
484     surfaceTexture.release();
485     eglBase.release();
486     handler.getLooper().quit();
487   }
488 }
489