blob: f11b70b6d6c20f7e1c90dd467fc7cf0cab46f80b [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;
Artem Titarenko69540f42018-12-10 12:30:46 +010021import android.support.annotation.Nullable;
nissec490e012015-12-10 06:23:33 -080022import java.nio.ByteBuffer;
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020023import java.util.concurrent.Callable;
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
Magnus Jedvert95140712018-11-15 12:07:32 +010039 * initialize a pixel buffer surface and make it current. If alignTimestamps is true, the frame
40 * timestamps will be aligned to rtc::TimeNanos(). If frame timestamps are aligned to
41 * rtc::TimeNanos() there is no need for aligning timestamps again in
42 * PeerConnectionFactory.createVideoSource(). This makes the timestamps more accurate and
43 * closer to actual creation time.
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020044 */
Åsa Perssonf2889bb2019-02-25 16:20:01 +010045 public static SurfaceTextureHelper create(final String threadName,
46 final EglBase.Context sharedContext, boolean alignTimestamps,
47 final YuvConverter yuvConverter) {
magjed82b750b2016-03-31 00:54:15 -070048 final HandlerThread thread = new HandlerThread(threadName);
magjed9e69dfd2016-02-26 07:45:44 -080049 thread.start();
50 final Handler handler = new Handler(thread.getLooper());
51
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020052 // The onFrameAvailable() callback will be executed on the SurfaceTexture ctor thread. See:
53 // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/graphics/SurfaceTexture.java#195.
54 // Therefore, in order to control the callback thread on API lvl < 21, the SurfaceTextureHelper
55 // is constructed on the |handler| thread.
nissea44e72c2016-05-27 00:27:59 -070056 return ThreadUtils.invokeAtFrontUninterruptibly(handler, new Callable<SurfaceTextureHelper>() {
Sami Kalliomäkie7592d82018-03-22 13:32:44 +010057 @Nullable
magjed9e69dfd2016-02-26 07:45:44 -080058 @Override
59 public SurfaceTextureHelper call() {
magjed2aa84262016-05-09 08:28:45 -070060 try {
Åsa Perssonf2889bb2019-02-25 16:20:01 +010061 return new SurfaceTextureHelper(sharedContext, handler, alignTimestamps, yuvConverter);
magjed2aa84262016-05-09 08:28:45 -070062 } catch (RuntimeException e) {
63 Logging.e(TAG, threadName + " create failure", e);
64 return null;
65 }
Magnus Jedvert747c1bc2015-10-12 09:27:48 +020066 }
67 });
68 }
69
Magnus Jedvert95140712018-11-15 12:07:32 +010070 /**
Åsa Perssonf2889bb2019-02-25 16:20:01 +010071 * Same as above with alignTimestamps set to false and yuvConverter set to new YuvConverter.
Magnus Jedvert95140712018-11-15 12:07:32 +010072 *
Åsa Perssonf2889bb2019-02-25 16:20:01 +010073 * @see #create(String, EglBase.Context, boolean, YuvConverter)
Magnus Jedvert95140712018-11-15 12:07:32 +010074 */
75 public static SurfaceTextureHelper create(
76 final String threadName, final EglBase.Context sharedContext) {
Åsa Perssonf2889bb2019-02-25 16:20:01 +010077 return create(threadName, sharedContext, /* alignTimestamps= */ false, new YuvConverter());
78 }
79
80 /**
81 * Same as above with yuvConverter set to new YuvConverter.
82 *
83 * @see #create(String, EglBase.Context, boolean, YuvConverter)
84 */
85 public static SurfaceTextureHelper create(
86 final String threadName, final EglBase.Context sharedContext, boolean alignTimestamps) {
87 return create(threadName, sharedContext, alignTimestamps, new YuvConverter());
Magnus Jedvert95140712018-11-15 12:07:32 +010088 }
89
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020090 private final Handler handler;
91 private final EglBase eglBase;
92 private final SurfaceTexture surfaceTexture;
93 private final int oesTextureId;
Åsa Perssonf2889bb2019-02-25 16:20:01 +010094 private final YuvConverter yuvConverter;
Magnus Jedvert95140712018-11-15 12:07:32 +010095 @Nullable private final TimestampAligner timestampAligner;
nissec490e012015-12-10 06:23:33 -080096
magjed81e8e372016-03-03 02:11:44 -080097 // These variables are only accessed from the |handler| thread.
Magnus Jedvert814f99c2018-08-03 11:15:14 +020098 @Nullable private VideoSink listener;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +020099 // The possible states of this class.
Sami Kalliomäki3d50a312018-09-11 11:11:47 +0200100 private boolean hasPendingTexture;
101 private volatile boolean isTextureInUse;
102 private boolean isQuitting;
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200103 private int frameRotation;
104 private int textureWidth;
105 private int textureHeight;
magjedd8ddb792016-03-17 03:13:43 -0700106 // |pendingListener| is set in setListener() and the runnable is posted to the handler thread.
107 // setListener() is not allowed to be called again before stopListening(), so this is thread safe.
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200108 @Nullable private VideoSink pendingListener;
magjedd8ddb792016-03-17 03:13:43 -0700109 final Runnable setListenerRunnable = new Runnable() {
110 @Override
111 public void run() {
112 Logging.d(TAG, "Setting listener to " + pendingListener);
113 listener = pendingListener;
114 pendingListener = null;
Magnus Jedvert181310f2016-05-23 16:26:47 +0200115 // May have a pending frame from the previous capture session - drop it.
116 if (hasPendingTexture) {
117 // Calling updateTexImage() is neccessary in order to receive new frames.
118 updateTexImage();
119 hasPendingTexture = false;
120 }
magjedd8ddb792016-03-17 03:13:43 -0700121 }
122 };
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200123
Åsa Perssonf2889bb2019-02-25 16:20:01 +0100124 private SurfaceTextureHelper(EglBase.Context sharedContext, Handler handler,
125 boolean alignTimestamps, YuvConverter yuvConverter) {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700126 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert747c1bc2015-10-12 09:27:48 +0200127 throw new IllegalStateException("SurfaceTextureHelper must be created on the handler thread");
perkj1b33da12015-10-05 21:06:41 +0200128 }
Magnus Jedvert747c1bc2015-10-12 09:27:48 +0200129 this.handler = handler;
Magnus Jedvert95140712018-11-15 12:07:32 +0100130 this.timestampAligner = alignTimestamps ? new TimestampAligner() : null;
Åsa Perssonf2889bb2019-02-25 16:20:01 +0100131 this.yuvConverter = yuvConverter;
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200132
nisse03f80eb2015-12-07 01:17:16 -0800133 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
magjed2aa84262016-05-09 08:28:45 -0700134 try {
135 // Both these statements have been observed to fail on rare occasions, see BUG=webrtc:5682.
136 eglBase.createDummyPbufferSurface();
137 eglBase.makeCurrent();
138 } catch (RuntimeException e) {
139 // Clean up before rethrowing the exception.
140 eglBase.release();
141 handler.getLooper().quit();
142 throw e;
143 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200144
145 oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
146 surfaceTexture = new SurfaceTexture(oesTextureId);
Jonathan Yu22384412017-08-16 13:25:45 -0700147 setOnFrameAvailableListener(surfaceTexture, (SurfaceTexture st) -> {
148 hasPendingTexture = true;
149 tryDeliverTextureFrame();
150 }, handler);
151 }
152
153 @TargetApi(21)
154 private static void setOnFrameAvailableListener(SurfaceTexture surfaceTexture,
155 SurfaceTexture.OnFrameAvailableListener listener, Handler handler) {
156 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
157 surfaceTexture.setOnFrameAvailableListener(listener, handler);
158 } else {
159 // The documentation states that the listener will be called on an arbitrary thread, but in
160 // pratice, it is always the thread on which the SurfaceTexture was constructed. There are
161 // assertions in place in case this ever changes. For API >= 21, we use the new API to
162 // explicitly specify the handler.
163 surfaceTexture.setOnFrameAvailableListener(listener);
164 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200165 }
166
167 /**
magjed81e8e372016-03-03 02:11:44 -0800168 * Start to stream textures to the given |listener|. If you need to change listener, you need to
169 * call stopListening() first.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200170 */
171 public void startListening(final VideoSink listener) {
magjedd8ddb792016-03-17 03:13:43 -0700172 if (this.listener != null || this.pendingListener != null) {
Per3e9eb4b2015-09-28 10:52:22 +0200173 throw new IllegalStateException("SurfaceTextureHelper listener has already been set.");
174 }
magjedd8ddb792016-03-17 03:13:43 -0700175 this.pendingListener = listener;
176 handler.post(setListenerRunnable);
Per3e9eb4b2015-09-28 10:52:22 +0200177 }
178
179 /**
magjed81e8e372016-03-03 02:11:44 -0800180 * Stop listening. The listener set in startListening() is guaranteded to not receive any more
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200181 * onFrame() callbacks after this function returns.
magjed81e8e372016-03-03 02:11:44 -0800182 */
183 public void stopListening() {
magjedd8ddb792016-03-17 03:13:43 -0700184 Logging.d(TAG, "stopListening()");
185 handler.removeCallbacks(setListenerRunnable);
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200186 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> {
187 listener = null;
188 pendingListener = null;
nissea44e72c2016-05-27 00:27:59 -0700189 });
magjed81e8e372016-03-03 02:11:44 -0800190 }
191
192 /**
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200193 * Use this function to set the texture size. Note, do not call setDefaultBufferSize() yourself
194 * since this class needs to be aware of the texture size.
195 */
196 public void setTextureSize(int textureWidth, int textureHeight) {
197 if (textureWidth <= 0) {
198 throw new IllegalArgumentException("Texture width must be positive, but was " + textureWidth);
199 }
200 if (textureHeight <= 0) {
201 throw new IllegalArgumentException(
202 "Texture height must be positive, but was " + textureHeight);
203 }
204 surfaceTexture.setDefaultBufferSize(textureWidth, textureHeight);
205 handler.post(() -> {
206 this.textureWidth = textureWidth;
207 this.textureHeight = textureHeight;
208 });
209 }
210
211 /** Set the rotation of the delivered frames. */
212 public void setFrameRotation(int rotation) {
213 handler.post(() -> this.frameRotation = rotation);
214 }
215
216 /**
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200217 * Retrieve the underlying SurfaceTexture. The SurfaceTexture should be passed in to a video
218 * producer such as a camera or decoder.
219 */
220 public SurfaceTexture getSurfaceTexture() {
221 return surfaceTexture;
222 }
223
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200224 /** Retrieve the handler that calls onFrame(). This handler is valid until dispose() is called. */
magjed9e69dfd2016-02-26 07:45:44 -0800225 public Handler getHandler() {
226 return handler;
227 }
228
229 /**
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200230 * This function is called when the texture frame is released. Only one texture frame can be in
231 * flight at once, so this function must be called before a new frame is delivered.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200232 */
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200233 private void returnTextureFrame() {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200234 handler.post(() -> {
235 isTextureInUse = false;
236 if (isQuitting) {
237 release();
238 } else {
239 tryDeliverTextureFrame();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200240 }
241 });
242 }
243
perkj88518a22015-12-18 00:37:06 -0800244 public boolean isTextureInUse() {
245 return isTextureInUse;
246 }
247
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200248 /**
magjed9e69dfd2016-02-26 07:45:44 -0800249 * Call disconnect() to stop receiving frames. OpenGL resources are released and the handler is
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200250 * stopped when the texture frame has been released. You are guaranteed to not receive any more
251 * onFrame() after this function returns.
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200252 */
magjed81e8e372016-03-03 02:11:44 -0800253 public void dispose() {
magjedd8ddb792016-03-17 03:13:43 -0700254 Logging.d(TAG, "dispose()");
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200255 ThreadUtils.invokeAtFrontUninterruptibly(handler, () -> {
256 isQuitting = true;
257 if (!isTextureInUse) {
258 release();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200259 }
260 });
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200261 }
262
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200263 /**
Magnus Jedvertc040dae2017-11-17 16:50:17 +0100264 * Posts to the correct thread to convert |textureBuffer| to I420.
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200265 *
266 * @deprecated Use toI420() instead.
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200267 */
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200268 @Deprecated
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200269 public VideoFrame.I420Buffer textureToYuv(final TextureBuffer textureBuffer) {
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200270 return textureBuffer.toI420();
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200271 }
272
Magnus Jedvert181310f2016-05-23 16:26:47 +0200273 private void updateTexImage() {
274 // SurfaceTexture.updateTexImage apparently can compete and deadlock with eglSwapBuffers,
275 // as observed on Nexus 5. Therefore, synchronize it with the EGL functions.
276 // See https://bugs.chromium.org/p/webrtc/issues/detail?id=5702 for more info.
277 synchronized (EglBase.lock) {
278 surfaceTexture.updateTexImage();
279 }
280 }
281
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200282 private void tryDeliverTextureFrame() {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700283 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200284 throw new IllegalStateException("Wrong thread.");
285 }
magjed81e8e372016-03-03 02:11:44 -0800286 if (isQuitting || !hasPendingTexture || isTextureInUse || listener == null) {
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200287 return;
288 }
289 isTextureInUse = true;
290 hasPendingTexture = false;
291
Magnus Jedvert181310f2016-05-23 16:26:47 +0200292 updateTexImage();
perkj1b33da12015-10-05 21:06:41 +0200293
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200294 final float[] transformMatrix = new float[16];
295 surfaceTexture.getTransformMatrix(transformMatrix);
Magnus Jedvert95140712018-11-15 12:07:32 +0100296 long timestampNs = surfaceTexture.getTimestamp();
297 if (timestampAligner != null) {
298 timestampNs = timestampAligner.translateTimestamp(timestampNs);
299 }
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200300 if (textureWidth == 0 || textureHeight == 0) {
301 throw new RuntimeException("Texture size has not been set.");
Magnus Jedvert80e7a7f2018-07-06 11:15:13 +0200302 }
Magnus Jedvert814f99c2018-08-03 11:15:14 +0200303 final VideoFrame.Buffer buffer =
304 new TextureBufferImpl(textureWidth, textureHeight, TextureBuffer.Type.OES, oesTextureId,
305 RendererCommon.convertMatrixToAndroidGraphicsMatrix(transformMatrix), handler,
306 yuvConverter, this ::returnTextureFrame);
307 final VideoFrame frame = new VideoFrame(buffer, frameRotation, timestampNs);
308 ((VideoSink) listener).onFrame(frame);
309 frame.release();
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200310 }
311
312 private void release() {
Alex Glazneva5b62d92015-10-12 13:56:20 -0700313 if (handler.getLooper().getThread() != Thread.currentThread()) {
Magnus Jedvert1ab271c2015-09-28 11:05:44 +0200314 throw new IllegalStateException("Wrong thread.");
315 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200316 if (isTextureInUse || !isQuitting) {
317 throw new IllegalStateException("Unexpected release.");
318 }
Magnus Jedvert1d270f82018-04-16 16:28:29 +0200319 yuvConverter.release();
Magnus Jedvert1ab271c2015-09-28 11:05:44 +0200320 GLES20.glDeleteTextures(1, new int[] {oesTextureId}, 0);
321 surfaceTexture.release();
322 eglBase.release();
perkj92375592015-11-24 03:03:13 -0800323 handler.getLooper().quit();
Magnus Jedvert95140712018-11-15 12:07:32 +0100324 if (timestampAligner != null) {
325 timestampAligner.dispose();
326 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200327 }
Magnus Jedvert4ae28a12015-09-15 09:44:07 +0200328}