blob: 146eb22437a6ce308cfc1038a71795ae314c3ab9 [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/**
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020028 * Helper class for using a SurfaceTexture to create WebRTC VideoFrames. In order to create WebRTC
29 * VideoFrames, render onto the SurfaceTexture. The frames will be delivered to the listener. Only
30 * one texture frame can be in flight at once, so the frame must be released in order to receive a
31 * new frame. Call stopListening() to stop receiveing new frames. Call dispose to release all
32 * resources once the texture frame is released.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020033 */
skvladed6e0772016-11-30 14:40:18 -080034public class SurfaceTextureHelper {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020035 private static final String TAG = "SurfaceTextureHelper";
36 /**
37 * Callback interface for being notified that a new texture frame is available. The calls will be
Jonathan Yu22384412017-08-16 13:25:45 -070038 * made on the SurfaceTextureHelper handler thread, with a bound EGLContext. The callee is not
39 * allowed to make another EGLContext current on the calling thread.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020040 *
41 * @deprecated Use a VideoSink as listener instead.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020042 */
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020043 @Deprecated
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020044 public interface OnTextureFrameAvailableListener {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020045 void onTextureFrameAvailable(int oesTextureId, float[] transformMatrix, long timestampNs);
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020046 }
47
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020048 /**
magjed9e69dfd2016-02-26 07:45:44 -080049 * Construct a new SurfaceTextureHelper sharing OpenGL resources with |sharedContext|. A dedicated
magjed2aa84262016-05-09 08:28:45 -070050 * thread and handler is created for handling the SurfaceTexture. May return null if EGL fails to
51 * initialize a pixel buffer surface and make it current.
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020052 */
magjed82b750b2016-03-31 00:54:15 -070053 public static SurfaceTextureHelper create(
54 final String threadName, final EglBase.Context sharedContext) {
55 final HandlerThread thread = new HandlerThread(threadName);
magjed9e69dfd2016-02-26 07:45:44 -080056 thread.start();
57 final Handler handler = new Handler(thread.getLooper());
58
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020059 // The onFrameAvailable() callback will be executed on the SurfaceTexture ctor thread. See:
60 // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/graphics/SurfaceTexture.java#195.
61 // Therefore, in order to control the callback thread on API lvl < 21, the SurfaceTextureHelper
62 // is constructed on the |handler| thread.
nissea44e72c2016-05-27 00:27:59 -070063 return ThreadUtils.invokeAtFrontUninterruptibly(handler, new Callable<SurfaceTextureHelper>() {
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010064 @Nullable
magjed9e69dfd2016-02-26 07:45:44 -080065 @Override
66 public SurfaceTextureHelper call() {
magjed2aa84262016-05-09 08:28:45 -070067 try {
68 return new SurfaceTextureHelper(sharedContext, handler);
69 } catch (RuntimeException e) {
70 Logging.e(TAG, threadName + " create failure", e);
71 return null;
72 }
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020073 }
74 });
75 }
76
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020077 private final Handler handler;
78 private final EglBase eglBase;
79 private final SurfaceTexture surfaceTexture;
80 private final int oesTextureId;
Magnus Jedvert1d270f82018-04-16 16:28:29 +020081 private final YuvConverter yuvConverter = new YuvConverter();
nissec490e012015-12-10 06:23:33 -080082
magjed81e8e372016-03-03 02:11:44 -080083 // These variables are only accessed from the |handler| thread.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020084 // The type of |listener| is either a VideoSink or the deprecated OnTextureFrameAvailableListener.
85 @Nullable private Object listener;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020086 // The possible states of this class.
87 private boolean hasPendingTexture = false;
perkj88518a22015-12-18 00:37:06 -080088 private volatile boolean isTextureInUse = false;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020089 private boolean isQuitting = false;
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020090 private int frameRotation;
91 private int textureWidth;
92 private int textureHeight;
magjedd8ddb792016-03-17 03:13:43 -070093 // |pendingListener| is set in setListener() and the runnable is posted to the handler thread.
94 // setListener() is not allowed to be called again before stopListening(), so this is thread safe.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020095 // The type of |pendingListener| is either a VideoSink or the deprecated
96 // OnTextureFrameAvailableListener.
97 @Nullable private Object pendingListener;
magjedd8ddb792016-03-17 03:13:43 -070098 final Runnable setListenerRunnable = new Runnable() {
99 @Override
100 public void run() {
101 Logging.d(TAG, "Setting listener to " + pendingListener);
102 listener = pendingListener;
103 pendingListener = null;
Magnus Jedvert181310f2016-05-23 16:26:47 +0200104 // May have a pending frame from the previous capture session - drop it.
105 if (hasPendingTexture) {
106 // Calling updateTexImage() is neccessary in order to receive new frames.
107 updateTexImage();
108 hasPendingTexture = false;
109 }
magjedd8ddb792016-03-17 03:13:43 -0700110 }
111 };
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200112
magjed9e69dfd2016-02-26 07:45:44 -0800113 private SurfaceTextureHelper(EglBase.Context sharedContext, Handler handler) {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700114 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert747c1bc2015-10-12 09:27:48 +0200115 throw new IllegalStateException("SurfaceTextureHelper must be created on the handler thread");
perkj1b33da12015-10-05 21:06:41 +0200116 }
Magnus Jedvert747c1bc2015-10-12 09:27:48 +0200117 this.handler = handler;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200118
nisse03f80eb2015-12-07 01:17:16 -0800119 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
magjed2aa84262016-05-09 08:28:45 -0700120 try {
121 // Both these statements have been observed to fail on rare occasions, see BUG=webrtc:5682.
122 eglBase.createDummyPbufferSurface();
123 eglBase.makeCurrent();
124 } catch (RuntimeException e) {
125 // Clean up before rethrowing the exception.
126 eglBase.release();
127 handler.getLooper().quit();
128 throw e;
129 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200130
131 oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
132 surfaceTexture = new SurfaceTexture(oesTextureId);
Jonathan Yu22384412017-08-16 13:25:45 -0700133 setOnFrameAvailableListener(surfaceTexture, (SurfaceTexture st) -> {
134 hasPendingTexture = true;
135 tryDeliverTextureFrame();
136 }, handler);
137 }
138
139 @TargetApi(21)
140 private static void setOnFrameAvailableListener(SurfaceTexture surfaceTexture,
141 SurfaceTexture.OnFrameAvailableListener listener, Handler handler) {
142 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
143 surfaceTexture.setOnFrameAvailableListener(listener, handler);
144 } else {
145 // The documentation states that the listener will be called on an arbitrary thread, but in
146 // pratice, it is always the thread on which the SurfaceTexture was constructed. There are
147 // assertions in place in case this ever changes. For API >= 21, we use the new API to
148 // explicitly specify the handler.
149 surfaceTexture.setOnFrameAvailableListener(listener);
150 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200151 }
152
153 /**
magjed81e8e372016-03-03 02:11:44 -0800154 * Start to stream textures to the given |listener|. If you need to change listener, you need to
155 * call stopListening() first.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200156 *
157 * @deprecated Use a VideoSink as listener instead.
Per3e9eb4b2015-09-28 10:52:22 +0200158 */
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200159 @Deprecated
magjed81e8e372016-03-03 02:11:44 -0800160 public void startListening(final OnTextureFrameAvailableListener listener) {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200161 startListeningInternal(listener);
162 }
163
164 /**
165 * Start to stream textures to the given |listener|. If you need to change listener, you need to
166 * call stopListening() first.
167 */
168 public void startListening(final VideoSink listener) {
169 startListeningInternal(listener);
170 }
171
172 private void startListeningInternal(Object listener) {
magjedd8ddb792016-03-17 03:13:43 -0700173 if (this.listener != null || this.pendingListener != null) {
Per3e9eb4b2015-09-28 10:52:22 +0200174 throw new IllegalStateException("SurfaceTextureHelper listener has already been set.");
175 }
magjedd8ddb792016-03-17 03:13:43 -0700176 this.pendingListener = listener;
177 handler.post(setListenerRunnable);
Per3e9eb4b2015-09-28 10:52:22 +0200178 }
179
180 /**
magjed81e8e372016-03-03 02:11:44 -0800181 * Stop listening. The listener set in startListening() is guaranteded to not receive any more
nissea44e72c2016-05-27 00:27:59 -0700182 * onTextureFrameAvailable() callbacks after this function returns.
magjed81e8e372016-03-03 02:11:44 -0800183 */
184 public void stopListening() {
magjedd8ddb792016-03-17 03:13:43 -0700185 Logging.d(TAG, "stopListening()");
186 handler.removeCallbacks(setListenerRunnable);
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200187 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> {
188 listener = null;
189 pendingListener = null;
nissea44e72c2016-05-27 00:27:59 -0700190 });
magjed81e8e372016-03-03 02:11:44 -0800191 }
192
193 /**
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200194 * Use this function to set the texture size. Note, do not call setDefaultBufferSize() yourself
195 * since this class needs to be aware of the texture size.
196 */
197 public void setTextureSize(int textureWidth, int textureHeight) {
198 if (textureWidth <= 0) {
199 throw new IllegalArgumentException("Texture width must be positive, but was " + textureWidth);
200 }
201 if (textureHeight <= 0) {
202 throw new IllegalArgumentException(
203 "Texture height must be positive, but was " + textureHeight);
204 }
205 surfaceTexture.setDefaultBufferSize(textureWidth, textureHeight);
206 handler.post(() -> {
207 this.textureWidth = textureWidth;
208 this.textureHeight = textureHeight;
209 });
210 }
211
212 /** Set the rotation of the delivered frames. */
213 public void setFrameRotation(int rotation) {
214 handler.post(() -> this.frameRotation = rotation);
215 }
216
217 /**
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200218 * Retrieve the underlying SurfaceTexture. The SurfaceTexture should be passed in to a video
219 * producer such as a camera or decoder.
220 */
221 public SurfaceTexture getSurfaceTexture() {
222 return surfaceTexture;
223 }
224
225 /**
magjed9e69dfd2016-02-26 07:45:44 -0800226 * Retrieve the handler that calls onTextureFrameAvailable(). This handler is valid until
magjed81e8e372016-03-03 02:11:44 -0800227 * dispose() is called.
magjed9e69dfd2016-02-26 07:45:44 -0800228 */
229 public Handler getHandler() {
230 return handler;
231 }
232
233 /**
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200234 * Call this function to signal that you are done with the frame received in
235 * onTextureFrameAvailable(). Only one texture frame can be in flight at once, so you must call
236 * this function in order to receive a new frame.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200237 *
238 * @deprecated Use a VideoSink as listener instead.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200239 */
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200240 @Deprecated
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200241 public void returnTextureFrame() {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200242 handler.post(() -> {
243 isTextureInUse = false;
244 if (isQuitting) {
245 release();
246 } else {
247 tryDeliverTextureFrame();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200248 }
249 });
250 }
251
perkj88518a22015-12-18 00:37:06 -0800252 public boolean isTextureInUse() {
253 return isTextureInUse;
254 }
255
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200256 /**
magjed9e69dfd2016-02-26 07:45:44 -0800257 * Call disconnect() to stop receiving frames. OpenGL resources are released and the handler is
258 * stopped when the texture frame has been returned by a call to returnTextureFrame(). You are
259 * guaranteed to not receive any more onTextureFrameAvailable() after this function returns.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200260 */
magjed81e8e372016-03-03 02:11:44 -0800261 public void dispose() {
magjedd8ddb792016-03-17 03:13:43 -0700262 Logging.d(TAG, "dispose()");
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200263 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> {
264 isQuitting = true;
265 if (!isTextureInUse) {
266 release();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200267 }
268 });
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200269 }
270
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200271 /**
Magnus Jedvertc040dae2017-11-17 16:50:17 +0100272 * Posts to the correct thread to convert |textureBuffer| to I420.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200273 *
274 * @deprecated Use toI420() instead.
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200275 */
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200276 @Deprecated
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200277 public VideoFrame.I420Buffer textureToYuv(final TextureBuffer textureBuffer) {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200278 return textureBuffer.toI420();
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200279 }
280
Magnus Jedvert181310f2016-05-23 16:26:47 +0200281 private void updateTexImage() {
282 // SurfaceTexture.updateTexImage apparently can compete and deadlock with eglSwapBuffers,
283 // as observed on Nexus 5. Therefore, synchronize it with the EGL functions.
284 // See https://bugs.chromium.org/p/webrtc/issues/detail?id=5702 for more info.
285 synchronized (EglBase.lock) {
286 surfaceTexture.updateTexImage();
287 }
288 }
289
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200290 private void tryDeliverTextureFrame() {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700291 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200292 throw new IllegalStateException("Wrong thread.");
293 }
magjed81e8e372016-03-03 02:11:44 -0800294 if (isQuitting || !hasPendingTexture || isTextureInUse || listener == null) {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200295 return;
296 }
297 isTextureInUse = true;
298 hasPendingTexture = false;
299
Magnus Jedvert181310f2016-05-23 16:26:47 +0200300 updateTexImage();
perkj1b33da12015-10-05 21:06:41 +0200301
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200302 final float[] transformMatrix = new float[16];
303 surfaceTexture.getTransformMatrix(transformMatrix);
sakalbee4ff82017-05-02 08:33:52 -0700304 final long timestampNs = surfaceTexture.getTimestamp();
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200305 if (listener instanceof OnTextureFrameAvailableListener) {
306 ((OnTextureFrameAvailableListener) listener)
307 .onTextureFrameAvailable(oesTextureId, transformMatrix, timestampNs);
308 } else if (listener instanceof VideoSink) {
309 if (textureWidth == 0 || textureHeight == 0) {
310 throw new RuntimeException("Texture size has not been set.");
311 }
312 final VideoFrame.Buffer buffer = createTextureBuffer(textureWidth, textureHeight,
313 RendererCommon.convertMatrixToAndroidGraphicsMatrix(transformMatrix));
314 final VideoFrame frame = new VideoFrame(buffer, frameRotation, timestampNs);
315 ((VideoSink) listener).onFrame(frame);
316 frame.release();
317 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200318 }
319
320 private void release() {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700321 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert1ab271c2015-09-28 11:05:44 +0200322 throw new IllegalStateException("Wrong thread.");
323 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200324 if (isTextureInUse || !isQuitting) {
325 throw new IllegalStateException("Unexpected release.");
326 }
Magnus Jedvert1d270f82018-04-16 16:28:29 +0200327 yuvConverter.release();
Magnus Jedvert1ab271c2015-09-28 11:05:44 +0200328 GLES20.glDeleteTextures(1, new int[] {oesTextureId}, 0);
329 surfaceTexture.release();
330 eglBase.release();
perkj92375592015-11-24 03:03:13 -0800331 handler.getLooper().quit();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200332 }
Bjorn Mellem8fb23612017-07-18 11:33:39 -0700333
334 /**
335 * Creates a VideoFrame buffer backed by this helper's texture. The |width| and |height| should
336 * match the dimensions of the data placed in the texture. The correct |transformMatrix| may be
337 * obtained from callbacks to OnTextureFrameAvailableListener.
338 *
339 * The returned TextureBuffer holds a reference to the SurfaceTextureHelper that created it. The
340 * buffer calls returnTextureFrame() when it is released.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200341 *
342 * @deprecated Use a VideoSink as listener instead.
Bjorn Mellem8fb23612017-07-18 11:33:39 -0700343 */
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200344 @Deprecated
Magnus Jedvert783c6e32018-07-05 13:34:17 +0200345 public TextureBufferImpl createTextureBuffer(int width, int height, Matrix transformMatrix) {
Magnus Jedvert1d270f82018-04-16 16:28:29 +0200346 return new TextureBufferImpl(width, height, TextureBuffer.Type.OES, oesTextureId,
347 transformMatrix, handler, yuvConverter, this ::returnTextureFrame);
Bjorn Mellem8fb23612017-07-18 11:33:39 -0700348 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200349}