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