blob: bb67d1f9fe0a62c70109a92482252c4c44de0e0b [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 /**
magjed9e69dfd2016-02-26 07:45:44 -080037 * Construct a new SurfaceTextureHelper sharing OpenGL resources with |sharedContext|. A dedicated
magjed2aa84262016-05-09 08:28:45 -070038 * thread and handler is created for handling the SurfaceTexture. May return null if EGL fails to
39 * initialize a pixel buffer surface and make it current.
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020040 */
magjed82b750b2016-03-31 00:54:15 -070041 public static SurfaceTextureHelper create(
42 final String threadName, final EglBase.Context sharedContext) {
43 final HandlerThread thread = new HandlerThread(threadName);
magjed9e69dfd2016-02-26 07:45:44 -080044 thread.start();
45 final Handler handler = new Handler(thread.getLooper());
46
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020047 // The onFrameAvailable() callback will be executed on the SurfaceTexture ctor thread. See:
48 // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/graphics/SurfaceTexture.java#195.
49 // Therefore, in order to control the callback thread on API lvl < 21, the SurfaceTextureHelper
50 // is constructed on the |handler| thread.
nissea44e72c2016-05-27 00:27:59 -070051 return ThreadUtils.invokeAtFrontUninterruptibly(handler, new Callable<SurfaceTextureHelper>() {
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010052 @Nullable
magjed9e69dfd2016-02-26 07:45:44 -080053 @Override
54 public SurfaceTextureHelper call() {
magjed2aa84262016-05-09 08:28:45 -070055 try {
56 return new SurfaceTextureHelper(sharedContext, handler);
57 } catch (RuntimeException e) {
58 Logging.e(TAG, threadName + " create failure", e);
59 return null;
60 }
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020061 }
62 });
63 }
64
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020065 private final Handler handler;
66 private final EglBase eglBase;
67 private final SurfaceTexture surfaceTexture;
68 private final int oesTextureId;
Magnus Jedvert1d270f82018-04-16 16:28:29 +020069 private final YuvConverter yuvConverter = new YuvConverter();
nissec490e012015-12-10 06:23:33 -080070
magjed81e8e372016-03-03 02:11:44 -080071 // These variables are only accessed from the |handler| thread.
Magnus Jedvert814f99c2018-08-03 11:15:14 +020072 @Nullable private VideoSink listener;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020073 // The possible states of this class.
Sami Kalliomäki3d50a312018-09-11 11:11:47 +020074 private boolean hasPendingTexture;
75 private volatile boolean isTextureInUse;
76 private boolean isQuitting;
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +020077 private int frameRotation;
78 private int textureWidth;
79 private int textureHeight;
magjedd8ddb792016-03-17 03:13:43 -070080 // |pendingListener| is set in setListener() and the runnable is posted to the handler thread.
81 // setListener() is not allowed to be called again before stopListening(), so this is thread safe.
Magnus Jedvert814f99c2018-08-03 11:15:14 +020082 @Nullable private VideoSink pendingListener;
magjedd8ddb792016-03-17 03:13:43 -070083 final Runnable setListenerRunnable = new Runnable() {
84 @Override
85 public void run() {
86 Logging.d(TAG, "Setting listener to " + pendingListener);
87 listener = pendingListener;
88 pendingListener = null;
Magnus Jedvert181310f2016-05-23 16:26:47 +020089 // May have a pending frame from the previous capture session - drop it.
90 if (hasPendingTexture) {
91 // Calling updateTexImage() is neccessary in order to receive new frames.
92 updateTexImage();
93 hasPendingTexture = false;
94 }
magjedd8ddb792016-03-17 03:13:43 -070095 }
96 };
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020097
magjed9e69dfd2016-02-26 07:45:44 -080098 private SurfaceTextureHelper(EglBase.Context sharedContext, Handler handler) {
Alex Glazneva5b62d92015-10-12 13:56:20 -070099 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert747c1bc2015-10-12 09:27:48 +0200100 throw new IllegalStateException("SurfaceTextureHelper must be created on the handler thread");
perkj1b33da12015-10-05 21:06:41 +0200101 }
Magnus Jedvert747c1bc2015-10-12 09:27:48 +0200102 this.handler = handler;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200103
nisse03f80eb2015-12-07 01:17:16 -0800104 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
magjed2aa84262016-05-09 08:28:45 -0700105 try {
106 // Both these statements have been observed to fail on rare occasions, see BUG=webrtc:5682.
107 eglBase.createDummyPbufferSurface();
108 eglBase.makeCurrent();
109 } catch (RuntimeException e) {
110 // Clean up before rethrowing the exception.
111 eglBase.release();
112 handler.getLooper().quit();
113 throw e;
114 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200115
116 oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
117 surfaceTexture = new SurfaceTexture(oesTextureId);
Jonathan Yu22384412017-08-16 13:25:45 -0700118 setOnFrameAvailableListener(surfaceTexture, (SurfaceTexture st) -> {
119 hasPendingTexture = true;
120 tryDeliverTextureFrame();
121 }, handler);
122 }
123
124 @TargetApi(21)
125 private static void setOnFrameAvailableListener(SurfaceTexture surfaceTexture,
126 SurfaceTexture.OnFrameAvailableListener listener, Handler handler) {
127 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
128 surfaceTexture.setOnFrameAvailableListener(listener, handler);
129 } else {
130 // The documentation states that the listener will be called on an arbitrary thread, but in
131 // pratice, it is always the thread on which the SurfaceTexture was constructed. There are
132 // assertions in place in case this ever changes. For API >= 21, we use the new API to
133 // explicitly specify the handler.
134 surfaceTexture.setOnFrameAvailableListener(listener);
135 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200136 }
137
138 /**
magjed81e8e372016-03-03 02:11:44 -0800139 * Start to stream textures to the given |listener|. If you need to change listener, you need to
140 * call stopListening() first.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200141 */
142 public void startListening(final VideoSink listener) {
magjedd8ddb792016-03-17 03:13:43 -0700143 if (this.listener != null || this.pendingListener != null) {
Per3e9eb4b2015-09-28 10:52:22 +0200144 throw new IllegalStateException("SurfaceTextureHelper listener has already been set.");
145 }
magjedd8ddb792016-03-17 03:13:43 -0700146 this.pendingListener = listener;
147 handler.post(setListenerRunnable);
Per3e9eb4b2015-09-28 10:52:22 +0200148 }
149
150 /**
magjed81e8e372016-03-03 02:11:44 -0800151 * Stop listening. The listener set in startListening() is guaranteded to not receive any more
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200152 * onFrame() callbacks after this function returns.
magjed81e8e372016-03-03 02:11:44 -0800153 */
154 public void stopListening() {
magjedd8ddb792016-03-17 03:13:43 -0700155 Logging.d(TAG, "stopListening()");
156 handler.removeCallbacks(setListenerRunnable);
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200157 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> {
158 listener = null;
159 pendingListener = null;
nissea44e72c2016-05-27 00:27:59 -0700160 });
magjed81e8e372016-03-03 02:11:44 -0800161 }
162
163 /**
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200164 * Use this function to set the texture size. Note, do not call setDefaultBufferSize() yourself
165 * since this class needs to be aware of the texture size.
166 */
167 public void setTextureSize(int textureWidth, int textureHeight) {
168 if (textureWidth <= 0) {
169 throw new IllegalArgumentException("Texture width must be positive, but was " + textureWidth);
170 }
171 if (textureHeight <= 0) {
172 throw new IllegalArgumentException(
173 "Texture height must be positive, but was " + textureHeight);
174 }
175 surfaceTexture.setDefaultBufferSize(textureWidth, textureHeight);
176 handler.post(() -> {
177 this.textureWidth = textureWidth;
178 this.textureHeight = textureHeight;
179 });
180 }
181
182 /** Set the rotation of the delivered frames. */
183 public void setFrameRotation(int rotation) {
184 handler.post(() -> this.frameRotation = rotation);
185 }
186
187 /**
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200188 * Retrieve the underlying SurfaceTexture. The SurfaceTexture should be passed in to a video
189 * producer such as a camera or decoder.
190 */
191 public SurfaceTexture getSurfaceTexture() {
192 return surfaceTexture;
193 }
194
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200195 /** Retrieve the handler that calls onFrame(). This handler is valid until dispose() is called. */
magjed9e69dfd2016-02-26 07:45:44 -0800196 public Handler getHandler() {
197 return handler;
198 }
199
200 /**
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200201 * This function is called when the texture frame is released. Only one texture frame can be in
202 * flight at once, so this function must be called before a new frame is delivered.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200203 */
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200204 private void returnTextureFrame() {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200205 handler.post(() -> {
206 isTextureInUse = false;
207 if (isQuitting) {
208 release();
209 } else {
210 tryDeliverTextureFrame();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200211 }
212 });
213 }
214
perkj88518a22015-12-18 00:37:06 -0800215 public boolean isTextureInUse() {
216 return isTextureInUse;
217 }
218
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200219 /**
magjed9e69dfd2016-02-26 07:45:44 -0800220 * Call disconnect() to stop receiving frames. OpenGL resources are released and the handler is
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200221 * stopped when the texture frame has been released. You are guaranteed to not receive any more
222 * onFrame() after this function returns.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200223 */
magjed81e8e372016-03-03 02:11:44 -0800224 public void dispose() {
magjedd8ddb792016-03-17 03:13:43 -0700225 Logging.d(TAG, "dispose()");
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200226 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> {
227 isQuitting = true;
228 if (!isTextureInUse) {
229 release();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200230 }
231 });
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200232 }
233
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200234 /**
Magnus Jedvertc040dae2017-11-17 16:50:17 +0100235 * Posts to the correct thread to convert |textureBuffer| to I420.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200236 *
237 * @deprecated Use toI420() instead.
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200238 */
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200239 @Deprecated
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200240 public VideoFrame.I420Buffer textureToYuv(final TextureBuffer textureBuffer) {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200241 return textureBuffer.toI420();
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200242 }
243
Magnus Jedvert181310f2016-05-23 16:26:47 +0200244 private void updateTexImage() {
245 // SurfaceTexture.updateTexImage apparently can compete and deadlock with eglSwapBuffers,
246 // as observed on Nexus 5. Therefore, synchronize it with the EGL functions.
247 // See https://bugs.chromium.org/p/webrtc/issues/detail?id=5702 for more info.
248 synchronized (EglBase.lock) {
249 surfaceTexture.updateTexImage();
250 }
251 }
252
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200253 private void tryDeliverTextureFrame() {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700254 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200255 throw new IllegalStateException("Wrong thread.");
256 }
magjed81e8e372016-03-03 02:11:44 -0800257 if (isQuitting || !hasPendingTexture || isTextureInUse || listener == null) {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200258 return;
259 }
260 isTextureInUse = true;
261 hasPendingTexture = false;
262
Magnus Jedvert181310f2016-05-23 16:26:47 +0200263 updateTexImage();
perkj1b33da12015-10-05 21:06:41 +0200264
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200265 final float[] transformMatrix = new float[16];
266 surfaceTexture.getTransformMatrix(transformMatrix);
sakalbee4ff82017-05-02 08:33:52 -0700267 final long timestampNs = surfaceTexture.getTimestamp();
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200268 if (textureWidth == 0 || textureHeight == 0) {
269 throw new RuntimeException("Texture size has not been set.");
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200270 }
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200271 final VideoFrame.Buffer buffer =
272 new TextureBufferImpl(textureWidth, textureHeight, TextureBuffer.Type.OES, oesTextureId,
273 RendererCommon.convertMatrixToAndroidGraphicsMatrix(transformMatrix), handler,
274 yuvConverter, this ::returnTextureFrame);
275 final VideoFrame frame = new VideoFrame(buffer, frameRotation, timestampNs);
276 ((VideoSink) listener).onFrame(frame);
277 frame.release();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200278 }
279
280 private void release() {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700281 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert1ab271c2015-09-28 11:05:44 +0200282 throw new IllegalStateException("Wrong thread.");
283 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200284 if (isTextureInUse || !isQuitting) {
285 throw new IllegalStateException("Unexpected release.");
286 }
Magnus Jedvert1d270f82018-04-16 16:28:29 +0200287 yuvConverter.release();
Magnus Jedvert1ab271c2015-09-28 11:05:44 +0200288 GLES20.glDeleteTextures(1, new int[] {oesTextureId}, 0);
289 surfaceTexture.release();
290 eglBase.release();
perkj92375592015-11-24 03:03:13 -0800291 handler.getLooper().quit();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200292 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200293}