blob: eefc153ff2a09c43875c39614ac8aa6ecf5f902e [file] [log] [blame]
Magnus Jedvert4ae28a12015-09-15 09:44:07 +02001/*
kjellanderb24317b2016-02-10 07:54:43 -08002 * Copyright 2015 The WebRTC project authors. All Rights Reserved.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +02003 *
kjellanderb24317b2016-02-10 07:54:43 -08004 * 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.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +02009 */
10
11package org.webrtc;
12
Jonathan Yu22384412017-08-16 13:25:45 -070013import android.annotation.TargetApi;
sakal836f60c2017-07-28 07:12:23 -070014import android.graphics.Matrix;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020015import android.graphics.SurfaceTexture;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020016import android.opengl.GLES11Ext;
17import android.opengl.GLES20;
18import android.os.Build;
19import android.os.Handler;
20import android.os.HandlerThread;
nissec490e012015-12-10 06:23:33 -080021import java.nio.ByteBuffer;
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020022import java.util.concurrent.Callable;
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010023import javax.annotation.Nullable;
Magnus Jedvert6199a372017-11-14 13:03:08 +010024import org.webrtc.EglBase;
Bjorn Mellem8fb23612017-07-18 11:33:39 -070025import org.webrtc.VideoFrame.TextureBuffer;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020026
27/**
28 * Helper class to create and synchronize access to a SurfaceTexture. The caller will get notified
29 * of new frames in onTextureFrameAvailable(), and should call returnTextureFrame() when done with
30 * the frame. Only one texture frame can be in flight at once, so returnTextureFrame() must be
magjed81e8e372016-03-03 02:11:44 -080031 * called in order to receive a new frame. Call stopListening() to stop receiveing new frames. Call
32 * dispose to release all resources once the texture frame is returned.
Per3e9eb4b2015-09-28 10:52:22 +020033 * Note that there is a C++ counter part of this class that optionally can be used. It is used for
34 * wrapping texture frames into webrtc::VideoFrames and also handles calling returnTextureFrame()
35 * when the webrtc::VideoFrame is no longer used.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020036 */
skvladed6e0772016-11-30 14:40:18 -080037public class SurfaceTextureHelper {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020038 private static final String TAG = "SurfaceTextureHelper";
39 /**
40 * Callback interface for being notified that a new texture frame is available. The calls will be
Jonathan Yu22384412017-08-16 13:25:45 -070041 * made on the SurfaceTextureHelper handler thread, with a bound EGLContext. The callee is not
42 * allowed to make another EGLContext current on the calling thread.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020043 */
44 public interface OnTextureFrameAvailableListener {
45 abstract void onTextureFrameAvailable(
46 int oesTextureId, float[] transformMatrix, long timestampNs);
47 }
48
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020049 /**
magjed9e69dfd2016-02-26 07:45:44 -080050 * Construct a new SurfaceTextureHelper sharing OpenGL resources with |sharedContext|. A dedicated
magjed2aa84262016-05-09 08:28:45 -070051 * thread and handler is created for handling the SurfaceTexture. May return null if EGL fails to
52 * initialize a pixel buffer surface and make it current.
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020053 */
Magnus Jedvert6199a372017-11-14 13:03:08 +010054 @CalledByNative
magjed82b750b2016-03-31 00:54:15 -070055 public static SurfaceTextureHelper create(
56 final String threadName, final EglBase.Context sharedContext) {
57 final HandlerThread thread = new HandlerThread(threadName);
magjed9e69dfd2016-02-26 07:45:44 -080058 thread.start();
59 final Handler handler = new Handler(thread.getLooper());
60
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020061 // The onFrameAvailable() callback will be executed on the SurfaceTexture ctor thread. See:
62 // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/graphics/SurfaceTexture.java#195.
63 // Therefore, in order to control the callback thread on API lvl < 21, the SurfaceTextureHelper
64 // is constructed on the |handler| thread.
nissea44e72c2016-05-27 00:27:59 -070065 return ThreadUtils.invokeAtFrontUninterruptibly(handler, new Callable<SurfaceTextureHelper>() {
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010066 @Nullable
magjed9e69dfd2016-02-26 07:45:44 -080067 @Override
68 public SurfaceTextureHelper call() {
magjed2aa84262016-05-09 08:28:45 -070069 try {
70 return new SurfaceTextureHelper(sharedContext, handler);
71 } catch (RuntimeException e) {
72 Logging.e(TAG, threadName + " create failure", e);
73 return null;
74 }
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020075 }
76 });
77 }
78
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020079 private final Handler handler;
80 private final EglBase eglBase;
81 private final SurfaceTexture surfaceTexture;
82 private final int oesTextureId;
nissec490e012015-12-10 06:23:33 -080083 private YuvConverter yuvConverter;
84
magjed81e8e372016-03-03 02:11:44 -080085 // These variables are only accessed from the |handler| thread.
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010086 @Nullable private OnTextureFrameAvailableListener listener;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020087 // The possible states of this class.
88 private boolean hasPendingTexture = false;
perkj88518a22015-12-18 00:37:06 -080089 private volatile boolean isTextureInUse = false;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020090 private boolean isQuitting = false;
magjedd8ddb792016-03-17 03:13:43 -070091 // |pendingListener| is set in setListener() and the runnable is posted to the handler thread.
92 // setListener() is not allowed to be called again before stopListening(), so this is thread safe.
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010093 @Nullable private OnTextureFrameAvailableListener pendingListener;
magjedd8ddb792016-03-17 03:13:43 -070094 final Runnable setListenerRunnable = new Runnable() {
95 @Override
96 public void run() {
97 Logging.d(TAG, "Setting listener to " + pendingListener);
98 listener = pendingListener;
99 pendingListener = null;
Magnus Jedvert181310f2016-05-23 16:26:47 +0200100 // May have a pending frame from the previous capture session - drop it.
101 if (hasPendingTexture) {
102 // Calling updateTexImage() is neccessary in order to receive new frames.
103 updateTexImage();
104 hasPendingTexture = false;
105 }
magjedd8ddb792016-03-17 03:13:43 -0700106 }
107 };
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200108
magjed9e69dfd2016-02-26 07:45:44 -0800109 private SurfaceTextureHelper(EglBase.Context sharedContext, Handler handler) {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700110 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert747c1bc2015-10-12 09:27:48 +0200111 throw new IllegalStateException("SurfaceTextureHelper must be created on the handler thread");
perkj1b33da12015-10-05 21:06:41 +0200112 }
Magnus Jedvert747c1bc2015-10-12 09:27:48 +0200113 this.handler = handler;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200114
nisse03f80eb2015-12-07 01:17:16 -0800115 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
magjed2aa84262016-05-09 08:28:45 -0700116 try {
117 // Both these statements have been observed to fail on rare occasions, see BUG=webrtc:5682.
118 eglBase.createDummyPbufferSurface();
119 eglBase.makeCurrent();
120 } catch (RuntimeException e) {
121 // Clean up before rethrowing the exception.
122 eglBase.release();
123 handler.getLooper().quit();
124 throw e;
125 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200126
127 oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
128 surfaceTexture = new SurfaceTexture(oesTextureId);
Jonathan Yu22384412017-08-16 13:25:45 -0700129 setOnFrameAvailableListener(surfaceTexture, (SurfaceTexture st) -> {
130 hasPendingTexture = true;
131 tryDeliverTextureFrame();
132 }, handler);
133 }
134
135 @TargetApi(21)
136 private static void setOnFrameAvailableListener(SurfaceTexture surfaceTexture,
137 SurfaceTexture.OnFrameAvailableListener listener, Handler handler) {
138 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
139 surfaceTexture.setOnFrameAvailableListener(listener, handler);
140 } else {
141 // The documentation states that the listener will be called on an arbitrary thread, but in
142 // pratice, it is always the thread on which the SurfaceTexture was constructed. There are
143 // assertions in place in case this ever changes. For API >= 21, we use the new API to
144 // explicitly specify the handler.
145 surfaceTexture.setOnFrameAvailableListener(listener);
146 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200147 }
148
149 /**
magjed81e8e372016-03-03 02:11:44 -0800150 * Start to stream textures to the given |listener|. If you need to change listener, you need to
151 * call stopListening() first.
Per3e9eb4b2015-09-28 10:52:22 +0200152 */
magjed81e8e372016-03-03 02:11:44 -0800153 public void startListening(final OnTextureFrameAvailableListener listener) {
magjedd8ddb792016-03-17 03:13:43 -0700154 if (this.listener != null || this.pendingListener != null) {
Per3e9eb4b2015-09-28 10:52:22 +0200155 throw new IllegalStateException("SurfaceTextureHelper listener has already been set.");
156 }
magjedd8ddb792016-03-17 03:13:43 -0700157 this.pendingListener = listener;
158 handler.post(setListenerRunnable);
Per3e9eb4b2015-09-28 10:52:22 +0200159 }
160
161 /**
magjed81e8e372016-03-03 02:11:44 -0800162 * Stop listening. The listener set in startListening() is guaranteded to not receive any more
nissea44e72c2016-05-27 00:27:59 -0700163 * onTextureFrameAvailable() callbacks after this function returns.
magjed81e8e372016-03-03 02:11:44 -0800164 */
165 public void stopListening() {
magjedd8ddb792016-03-17 03:13:43 -0700166 Logging.d(TAG, "stopListening()");
167 handler.removeCallbacks(setListenerRunnable);
nissea44e72c2016-05-27 00:27:59 -0700168 ThreadUtils.invokeAtFrontUninterruptibly(handler, new Runnable() {
169 @Override
170 public void run() {
171 listener = null;
172 pendingListener = null;
173 }
174 });
magjed81e8e372016-03-03 02:11:44 -0800175 }
176
177 /**
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200178 * Retrieve the underlying SurfaceTexture. The SurfaceTexture should be passed in to a video
179 * producer such as a camera or decoder.
180 */
181 public SurfaceTexture getSurfaceTexture() {
182 return surfaceTexture;
183 }
184
185 /**
magjed9e69dfd2016-02-26 07:45:44 -0800186 * Retrieve the handler that calls onTextureFrameAvailable(). This handler is valid until
magjed81e8e372016-03-03 02:11:44 -0800187 * dispose() is called.
magjed9e69dfd2016-02-26 07:45:44 -0800188 */
189 public Handler getHandler() {
190 return handler;
191 }
192
193 /**
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200194 * Call this function to signal that you are done with the frame received in
195 * onTextureFrameAvailable(). Only one texture frame can be in flight at once, so you must call
196 * this function in order to receive a new frame.
197 */
Magnus Jedvert6199a372017-11-14 13:03:08 +0100198 @CalledByNative
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200199 public void returnTextureFrame() {
200 handler.post(new Runnable() {
sakalb6760f92016-09-29 04:12:44 -0700201 @Override
202 public void run() {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200203 isTextureInUse = false;
204 if (isQuitting) {
205 release();
206 } else {
207 tryDeliverTextureFrame();
208 }
209 }
210 });
211 }
212
perkj88518a22015-12-18 00:37:06 -0800213 public boolean isTextureInUse() {
214 return isTextureInUse;
215 }
216
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200217 /**
magjed9e69dfd2016-02-26 07:45:44 -0800218 * Call disconnect() to stop receiving frames. OpenGL resources are released and the handler is
219 * stopped when the texture frame has been returned by a call to returnTextureFrame(). You are
220 * guaranteed to not receive any more onTextureFrameAvailable() after this function returns.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200221 */
Magnus Jedvert6199a372017-11-14 13:03:08 +0100222 @CalledByNative
magjed81e8e372016-03-03 02:11:44 -0800223 public void dispose() {
magjedd8ddb792016-03-17 03:13:43 -0700224 Logging.d(TAG, "dispose()");
nissea44e72c2016-05-27 00:27:59 -0700225 ThreadUtils.invokeAtFrontUninterruptibly(handler, new Runnable() {
226 @Override
227 public void run() {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200228 isQuitting = true;
229 if (!isTextureInUse) {
230 release();
231 }
232 }
233 });
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200234 }
235
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200236 /** Deprecated, use textureToYuv. */
237 @Deprecated
238 @SuppressWarnings("deprecation") // yuvConverter.convert is deprecated
Magnus Jedvert9060eb12017-12-12 12:52:54 +0100239 @CalledByNative
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200240 void textureToYUV(final ByteBuffer buf, final int width, final int height, final int stride,
241 final int textureId, final float[] transformMatrix) {
magjed1cb48232016-10-20 03:19:16 -0700242 if (textureId != oesTextureId) {
nissec490e012015-12-10 06:23:33 -0800243 throw new IllegalStateException("textureToByteBuffer called with unexpected textureId");
magjed1cb48232016-10-20 03:19:16 -0700244 }
nissec490e012015-12-10 06:23:33 -0800245
magjed1cb48232016-10-20 03:19:16 -0700246 ThreadUtils.invokeAtFrontUninterruptibly(handler, new Runnable() {
247 @Override
248 public void run() {
249 if (yuvConverter == null) {
250 yuvConverter = new YuvConverter();
251 }
252 yuvConverter.convert(buf, width, height, stride, textureId, transformMatrix);
253 }
254 });
nissec490e012015-12-10 06:23:33 -0800255 }
256
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200257 /**
Magnus Jedvertc040dae2017-11-17 16:50:17 +0100258 * Posts to the correct thread to convert |textureBuffer| to I420.
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200259 */
260 public VideoFrame.I420Buffer textureToYuv(final TextureBuffer textureBuffer) {
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200261 final VideoFrame.I420Buffer[] result = new VideoFrame.I420Buffer[1];
262 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> {
263 if (yuvConverter == null) {
264 yuvConverter = new YuvConverter();
265 }
266 result[0] = yuvConverter.convert(textureBuffer);
267 });
268 return result[0];
269 }
270
Magnus Jedvert181310f2016-05-23 16:26:47 +0200271 private void updateTexImage() {
272 // SurfaceTexture.updateTexImage apparently can compete and deadlock with eglSwapBuffers,
273 // as observed on Nexus 5. Therefore, synchronize it with the EGL functions.
274 // See https://bugs.chromium.org/p/webrtc/issues/detail?id=5702 for more info.
275 synchronized (EglBase.lock) {
276 surfaceTexture.updateTexImage();
277 }
278 }
279
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200280 private void tryDeliverTextureFrame() {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700281 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200282 throw new IllegalStateException("Wrong thread.");
283 }
magjed81e8e372016-03-03 02:11:44 -0800284 if (isQuitting || !hasPendingTexture || isTextureInUse || listener == null) {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200285 return;
286 }
287 isTextureInUse = true;
288 hasPendingTexture = false;
289
Magnus Jedvert181310f2016-05-23 16:26:47 +0200290 updateTexImage();
perkj1b33da12015-10-05 21:06:41 +0200291
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200292 final float[] transformMatrix = new float[16];
293 surfaceTexture.getTransformMatrix(transformMatrix);
sakalbee4ff82017-05-02 08:33:52 -0700294 final long timestampNs = surfaceTexture.getTimestamp();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200295 listener.onTextureFrameAvailable(oesTextureId, transformMatrix, timestampNs);
296 }
297
298 private void release() {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700299 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert1ab271c2015-09-28 11:05:44 +0200300 throw new IllegalStateException("Wrong thread.");
301 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200302 if (isTextureInUse || !isQuitting) {
303 throw new IllegalStateException("Unexpected release.");
304 }
magjed1cb48232016-10-20 03:19:16 -0700305 if (yuvConverter != null) {
306 yuvConverter.release();
nissec490e012015-12-10 06:23:33 -0800307 }
Magnus Jedvert1ab271c2015-09-28 11:05:44 +0200308 GLES20.glDeleteTextures(1, new int[] {oesTextureId}, 0);
309 surfaceTexture.release();
310 eglBase.release();
perkj92375592015-11-24 03:03:13 -0800311 handler.getLooper().quit();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200312 }
Bjorn Mellem8fb23612017-07-18 11:33:39 -0700313
314 /**
315 * Creates a VideoFrame buffer backed by this helper's texture. The |width| and |height| should
316 * match the dimensions of the data placed in the texture. The correct |transformMatrix| may be
317 * obtained from callbacks to OnTextureFrameAvailableListener.
318 *
319 * The returned TextureBuffer holds a reference to the SurfaceTextureHelper that created it. The
320 * buffer calls returnTextureFrame() when it is released.
321 */
sakal836f60c2017-07-28 07:12:23 -0700322 public TextureBuffer createTextureBuffer(int width, int height, Matrix transformMatrix) {
Sami Kalliomäki64051d42018-04-16 13:37:07 +0000323 return new TextureBufferImpl(
324 width, height, TextureBuffer.Type.OES, oesTextureId, transformMatrix, this, new Runnable() {
325 @Override
326 public void run() {
327 returnTextureFrame();
328 }
329 });
Bjorn Mellem8fb23612017-07-18 11:33:39 -0700330 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200331}