Home | History | Annotate | Download | only in webrtc
      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 {
     64     abstract void onTextureFrameAvailable(
     65         int oesTextureId, float[] transformMatrix, long timestampNs);
     66   }
     67 
     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    */
     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