xref: /aosp_15_r20/external/webrtc/sdk/android/api/org/webrtc/SurfaceTextureHelper.java (revision d9f758449e529ab9291ac668be2861e7a55c2422)
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