1 /* 2 * Copyright 2015 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.webrtc; 12 13 import android.annotation.TargetApi; 14 import android.graphics.SurfaceTexture; 15 import android.opengl.GLES11Ext; 16 import android.opengl.GLES20; 17 import android.os.Build; 18 import android.os.Handler; 19 import android.os.HandlerThread; 20 import androidx.annotation.Nullable; 21 import java.util.concurrent.Callable; 22 import org.webrtc.EglBase.Context; 23 import org.webrtc.TextureBufferImpl.RefCountMonitor; 24 import org.webrtc.VideoFrame.TextureBuffer; 25 26 /** 27 * Helper class for using a SurfaceTexture to create WebRTC VideoFrames. In order to create WebRTC 28 * VideoFrames, render onto the SurfaceTexture. The frames will be delivered to the listener. Only 29 * one texture frame can be in flight at once, so the frame must be released in order to receive a 30 * new frame. Call stopListening() to stop receiveing new frames. Call dispose to release all 31 * resources once the texture frame is released. 32 */ 33 public class SurfaceTextureHelper { 34 /** 35 * Interface for monitoring texture buffers created from this SurfaceTexture. Since only one 36 * texture buffer can exist at a time, this can be used to monitor for stuck frames. 37 */ 38 public interface FrameRefMonitor { 39 /** A new frame was created. New frames start with ref count of 1. */ onNewBuffer(TextureBuffer textureBuffer)40 void onNewBuffer(TextureBuffer textureBuffer); 41 /** Ref count of the frame was incremented by the calling thread. */ onRetainBuffer(TextureBuffer textureBuffer)42 void onRetainBuffer(TextureBuffer textureBuffer); 43 /** Ref count of the frame was decremented by the calling thread. */ onReleaseBuffer(TextureBuffer textureBuffer)44 void onReleaseBuffer(TextureBuffer textureBuffer); 45 /** Frame was destroyed (ref count reached 0). */ onDestroyBuffer(TextureBuffer textureBuffer)46 void onDestroyBuffer(TextureBuffer textureBuffer); 47 } 48 49 private static final String TAG = "SurfaceTextureHelper"; 50 /** 51 * Construct a new SurfaceTextureHelper sharing OpenGL resources with `sharedContext`. A dedicated 52 * thread and handler is created for handling the SurfaceTexture. May return null if EGL fails to 53 * initialize a pixel buffer surface and make it current. If alignTimestamps is true, the frame 54 * timestamps will be aligned to rtc::TimeNanos(). If frame timestamps are aligned to 55 * rtc::TimeNanos() there is no need for aligning timestamps again in 56 * PeerConnectionFactory.createVideoSource(). This makes the timestamps more accurate and 57 * closer to actual creation time. 58 */ create(final String threadName, final EglBase.Context sharedContext, boolean alignTimestamps, final YuvConverter yuvConverter, FrameRefMonitor frameRefMonitor)59 public static SurfaceTextureHelper create(final String threadName, 60 final EglBase.Context sharedContext, boolean alignTimestamps, final YuvConverter yuvConverter, 61 FrameRefMonitor frameRefMonitor) { 62 final HandlerThread thread = new HandlerThread(threadName); 63 thread.start(); 64 final Handler handler = new Handler(thread.getLooper()); 65 66 // The onFrameAvailable() callback will be executed on the SurfaceTexture ctor thread. See: 67 // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/graphics/SurfaceTexture.java#195. 68 // Therefore, in order to control the callback thread on API lvl < 21, the SurfaceTextureHelper 69 // is constructed on the `handler` thread. 70 return ThreadUtils.invokeAtFrontUninterruptibly(handler, new Callable<SurfaceTextureHelper>() { 71 @Nullable 72 @Override 73 public SurfaceTextureHelper call() { 74 try { 75 return new SurfaceTextureHelper( 76 sharedContext, handler, alignTimestamps, yuvConverter, frameRefMonitor); 77 } catch (RuntimeException e) { 78 Logging.e(TAG, threadName + " create failure", e); 79 return null; 80 } 81 } 82 }); 83 } 84 85 /** 86 * Same as above with alignTimestamps set to false and yuvConverter set to new YuvConverter. 87 * 88 * @see #create(String, EglBase.Context, boolean, YuvConverter, FrameRefMonitor) 89 */ 90 public static SurfaceTextureHelper create( 91 final String threadName, final EglBase.Context sharedContext) { 92 return create(threadName, sharedContext, /* alignTimestamps= */ false, new YuvConverter(), 93 /*frameRefMonitor=*/null); 94 } 95 96 /** 97 * Same as above with yuvConverter set to new YuvConverter. 98 * 99 * @see #create(String, EglBase.Context, boolean, YuvConverter, FrameRefMonitor) 100 */ 101 public static SurfaceTextureHelper create( 102 final String threadName, final EglBase.Context sharedContext, boolean alignTimestamps) { 103 return create( 104 threadName, sharedContext, alignTimestamps, new YuvConverter(), /*frameRefMonitor=*/null); 105 } 106 107 /** 108 * Create a SurfaceTextureHelper without frame ref monitor. 109 * 110 * @see #create(String, EglBase.Context, boolean, YuvConverter, FrameRefMonitor) 111 */ 112 public static SurfaceTextureHelper create(final String threadName, 113 final EglBase.Context sharedContext, boolean alignTimestamps, YuvConverter yuvConverter) { 114 return create( 115 threadName, sharedContext, alignTimestamps, yuvConverter, /*frameRefMonitor=*/null); 116 } 117 118 private final RefCountMonitor textureRefCountMonitor = new RefCountMonitor() { 119 @Override 120 public void onRetain(TextureBufferImpl textureBuffer) { 121 if (frameRefMonitor != null) { 122 frameRefMonitor.onRetainBuffer(textureBuffer); 123 } 124 } 125 126 @Override 127 public void onRelease(TextureBufferImpl textureBuffer) { 128 if (frameRefMonitor != null) { 129 frameRefMonitor.onReleaseBuffer(textureBuffer); 130 } 131 } 132 133 @Override 134 public void onDestroy(TextureBufferImpl textureBuffer) { 135 returnTextureFrame(); 136 if (frameRefMonitor != null) { 137 frameRefMonitor.onDestroyBuffer(textureBuffer); 138 } 139 } 140 }; 141 142 private final Handler handler; 143 private final EglBase eglBase; 144 private final SurfaceTexture surfaceTexture; 145 private final int oesTextureId; 146 private final YuvConverter yuvConverter; 147 @Nullable private final TimestampAligner timestampAligner; 148 private final FrameRefMonitor frameRefMonitor; 149 150 // These variables are only accessed from the `handler` thread. 151 @Nullable private VideoSink listener; 152 // The possible states of this class. 153 private boolean hasPendingTexture; 154 private volatile boolean isTextureInUse; 155 private boolean isQuitting; 156 private int frameRotation; 157 private int textureWidth; 158 private int textureHeight; 159 // `pendingListener` is set in setListener() and the runnable is posted to the handler thread. 160 // setListener() is not allowed to be called again before stopListening(), so this is thread safe. 161 @Nullable private VideoSink pendingListener; 162 final Runnable setListenerRunnable = new Runnable() { 163 @Override 164 public void run() { 165 Logging.d(TAG, "Setting listener to " + pendingListener); 166 listener = pendingListener; 167 pendingListener = null; 168 // May have a pending frame from the previous capture session - drop it. 169 if (hasPendingTexture) { 170 // Calling updateTexImage() is neccessary in order to receive new frames. 171 updateTexImage(); 172 hasPendingTexture = false; 173 } 174 } 175 }; 176 177 private SurfaceTextureHelper(Context sharedContext, Handler handler, boolean alignTimestamps, 178 YuvConverter yuvConverter, FrameRefMonitor frameRefMonitor) { 179 if (handler.getLooper().getThread() != Thread.currentThread()) { 180 throw new IllegalStateException("SurfaceTextureHelper must be created on the handler thread"); 181 } 182 this.handler = handler; 183 this.timestampAligner = alignTimestamps ? new TimestampAligner() : null; 184 this.yuvConverter = yuvConverter; 185 this.frameRefMonitor = frameRefMonitor; 186 187 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER); 188 try { 189 // Both these statements have been observed to fail on rare occasions, see BUG=webrtc:5682. 190 eglBase.createDummyPbufferSurface(); 191 eglBase.makeCurrent(); 192 } catch (RuntimeException e) { 193 // Clean up before rethrowing the exception. 194 eglBase.release(); 195 handler.getLooper().quit(); 196 throw e; 197 } 198 199 oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); 200 surfaceTexture = new SurfaceTexture(oesTextureId); 201 surfaceTexture.setOnFrameAvailableListener(st -> { 202 if (hasPendingTexture) { 203 Logging.d(TAG, "A frame is already pending, dropping frame."); 204 } 205 206 hasPendingTexture = true; 207 tryDeliverTextureFrame(); 208 }, handler); 209 } 210 211 /** 212 * Start to stream textures to the given `listener`. If you need to change listener, you need to 213 * call stopListening() first. 214 */ 215 public void startListening(final VideoSink listener) { 216 if (this.listener != null || this.pendingListener != null) { 217 throw new IllegalStateException("SurfaceTextureHelper listener has already been set."); 218 } 219 this.pendingListener = listener; 220 handler.post(setListenerRunnable); 221 } 222 223 /** 224 * Stop listening. The listener set in startListening() is guaranteded to not receive any more 225 * onFrame() callbacks after this function returns. 226 */ 227 public void stopListening() { 228 Logging.d(TAG, "stopListening()"); 229 handler.removeCallbacks(setListenerRunnable); 230 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> { 231 listener = null; 232 pendingListener = null; 233 }); 234 } 235 236 /** 237 * Use this function to set the texture size. Note, do not call setDefaultBufferSize() yourself 238 * since this class needs to be aware of the texture size. 239 */ 240 public void setTextureSize(int textureWidth, int textureHeight) { 241 if (textureWidth <= 0) { 242 throw new IllegalArgumentException("Texture width must be positive, but was " + textureWidth); 243 } 244 if (textureHeight <= 0) { 245 throw new IllegalArgumentException( 246 "Texture height must be positive, but was " + textureHeight); 247 } 248 surfaceTexture.setDefaultBufferSize(textureWidth, textureHeight); 249 handler.post(() -> { 250 this.textureWidth = textureWidth; 251 this.textureHeight = textureHeight; 252 tryDeliverTextureFrame(); 253 }); 254 } 255 256 /** 257 * Forces a frame to be produced. If no new frame is available, the last frame is sent to the 258 * listener again. 259 */ 260 public void forceFrame() { 261 handler.post(() -> { 262 hasPendingTexture = true; 263 tryDeliverTextureFrame(); 264 }); 265 } 266 267 /** Set the rotation of the delivered frames. */ 268 public void setFrameRotation(int rotation) { 269 handler.post(() -> this.frameRotation = rotation); 270 } 271 272 /** 273 * Retrieve the underlying SurfaceTexture. The SurfaceTexture should be passed in to a video 274 * producer such as a camera or decoder. 275 */ 276 public SurfaceTexture getSurfaceTexture() { 277 return surfaceTexture; 278 } 279 280 /** Retrieve the handler that calls onFrame(). This handler is valid until dispose() is called. */ 281 public Handler getHandler() { 282 return handler; 283 } 284 285 /** 286 * This function is called when the texture frame is released. Only one texture frame can be in 287 * flight at once, so this function must be called before a new frame is delivered. 288 */ 289 private void returnTextureFrame() { 290 handler.post(() -> { 291 isTextureInUse = false; 292 if (isQuitting) { 293 release(); 294 } else { 295 tryDeliverTextureFrame(); 296 } 297 }); 298 } 299 300 public boolean isTextureInUse() { 301 return isTextureInUse; 302 } 303 304 /** 305 * Call disconnect() to stop receiving frames. OpenGL resources are released and the handler is 306 * stopped when the texture frame has been released. You are guaranteed to not receive any more 307 * onFrame() after this function returns. 308 */ 309 public void dispose() { 310 Logging.d(TAG, "dispose()"); 311 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> { 312 isQuitting = true; 313 if (!isTextureInUse) { 314 release(); 315 } 316 }); 317 } 318 319 /** 320 * Posts to the correct thread to convert `textureBuffer` to I420. 321 * 322 * @deprecated Use toI420() instead. 323 */ 324 @Deprecated 325 public VideoFrame.I420Buffer textureToYuv(final TextureBuffer textureBuffer) { 326 return textureBuffer.toI420(); 327 } 328 329 private void updateTexImage() { 330 // SurfaceTexture.updateTexImage apparently can compete and deadlock with eglSwapBuffers, 331 // as observed on Nexus 5. Therefore, synchronize it with the EGL functions. 332 // See https://bugs.chromium.org/p/webrtc/issues/detail?id=5702 for more info. 333 synchronized (EglBase.lock) { 334 surfaceTexture.updateTexImage(); 335 } 336 } 337 338 private void tryDeliverTextureFrame() { 339 if (handler.getLooper().getThread() != Thread.currentThread()) { 340 throw new IllegalStateException("Wrong thread."); 341 } 342 if (isQuitting || !hasPendingTexture || isTextureInUse || listener == null) { 343 return; 344 } 345 if (textureWidth == 0 || textureHeight == 0) { 346 // Information about the resolution needs to be provided by a call to setTextureSize() before 347 // frames are produced. 348 Logging.w(TAG, "Texture size has not been set."); 349 return; 350 } 351 isTextureInUse = true; 352 hasPendingTexture = false; 353 354 updateTexImage(); 355 356 final float[] transformMatrix = new float[16]; 357 surfaceTexture.getTransformMatrix(transformMatrix); 358 long timestampNs = surfaceTexture.getTimestamp(); 359 if (timestampAligner != null) { 360 timestampNs = timestampAligner.translateTimestamp(timestampNs); 361 } 362 final VideoFrame.TextureBuffer buffer = 363 new TextureBufferImpl(textureWidth, textureHeight, TextureBuffer.Type.OES, oesTextureId, 364 RendererCommon.convertMatrixToAndroidGraphicsMatrix(transformMatrix), handler, 365 yuvConverter, textureRefCountMonitor); 366 if (frameRefMonitor != null) { 367 frameRefMonitor.onNewBuffer(buffer); 368 } 369 final VideoFrame frame = new VideoFrame(buffer, frameRotation, timestampNs); 370 listener.onFrame(frame); 371 frame.release(); 372 } 373 374 private void release() { 375 if (handler.getLooper().getThread() != Thread.currentThread()) { 376 throw new IllegalStateException("Wrong thread."); 377 } 378 if (isTextureInUse || !isQuitting) { 379 throw new IllegalStateException("Unexpected release."); 380 } 381 yuvConverter.release(); 382 GLES20.glDeleteTextures(1, new int[] {oesTextureId}, 0); 383 surfaceTexture.release(); 384 eglBase.release(); 385 handler.getLooper().quit(); 386 if (timestampAligner != null) { 387 timestampAligner.dispose(); 388 } 389 } 390 } 391