• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Copyright 2016 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.graphics.Bitmap;
14 import android.graphics.Matrix;
15 import android.graphics.SurfaceTexture;
16 import android.opengl.GLES20;
17 import android.os.Handler;
18 import android.os.HandlerThread;
19 import android.os.Looper;
20 import android.os.Message;
21 import android.view.Surface;
22 import androidx.annotation.Nullable;
23 import java.nio.ByteBuffer;
24 import java.text.DecimalFormat;
25 import java.util.ArrayList;
26 import java.util.Iterator;
27 import java.util.concurrent.CountDownLatch;
28 import java.util.concurrent.TimeUnit;
29 
30 /**
31  * Implements VideoSink by displaying the video stream on an EGL Surface. This class is intended to
32  * be used as a helper class for rendering on SurfaceViews and TextureViews.
33  */
34 public class EglRenderer implements VideoSink {
35   private static final String TAG = "EglRenderer";
36   private static final long LOG_INTERVAL_SEC = 4;
37 
onFrame(Bitmap frame)38   public interface FrameListener { void onFrame(Bitmap frame); }
39 
40   /** Callback for clients to be notified about errors encountered during rendering. */
41   public static interface ErrorCallback {
42     /** Called if GLES20.GL_OUT_OF_MEMORY is encountered during rendering. */
onGlOutOfMemory()43     void onGlOutOfMemory();
44   }
45 
46   private static class FrameListenerAndParams {
47     public final FrameListener listener;
48     public final float scale;
49     public final RendererCommon.GlDrawer drawer;
50     public final boolean applyFpsReduction;
51 
FrameListenerAndParams(FrameListener listener, float scale, RendererCommon.GlDrawer drawer, boolean applyFpsReduction)52     public FrameListenerAndParams(FrameListener listener, float scale,
53         RendererCommon.GlDrawer drawer, boolean applyFpsReduction) {
54       this.listener = listener;
55       this.scale = scale;
56       this.drawer = drawer;
57       this.applyFpsReduction = applyFpsReduction;
58     }
59   }
60 
61   private class EglSurfaceCreation implements Runnable {
62     private Object surface;
63 
64     // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
65     @SuppressWarnings("NoSynchronizedMethodCheck")
setSurface(Object surface)66     public synchronized void setSurface(Object surface) {
67       this.surface = surface;
68     }
69 
70     @Override
71     // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
72     @SuppressWarnings("NoSynchronizedMethodCheck")
run()73     public synchronized void run() {
74       if (surface != null && eglBase != null && !eglBase.hasSurface()) {
75         if (surface instanceof Surface) {
76           eglBase.createSurface((Surface) surface);
77         } else if (surface instanceof SurfaceTexture) {
78           eglBase.createSurface((SurfaceTexture) surface);
79         } else {
80           throw new IllegalStateException("Invalid surface: " + surface);
81         }
82         eglBase.makeCurrent();
83         // Necessary for YUV frames with odd width.
84         GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
85       }
86     }
87   }
88 
89   /**
90    * Handler that triggers a callback when an uncaught exception happens when handling a message.
91    */
92   private static class HandlerWithExceptionCallback extends Handler {
93     private final Runnable exceptionCallback;
94 
HandlerWithExceptionCallback(Looper looper, Runnable exceptionCallback)95     public HandlerWithExceptionCallback(Looper looper, Runnable exceptionCallback) {
96       super(looper);
97       this.exceptionCallback = exceptionCallback;
98     }
99 
100     @Override
dispatchMessage(Message msg)101     public void dispatchMessage(Message msg) {
102       try {
103         super.dispatchMessage(msg);
104       } catch (Exception e) {
105         Logging.e(TAG, "Exception on EglRenderer thread", e);
106         exceptionCallback.run();
107         throw e;
108       }
109     }
110   }
111 
112   protected final String name;
113 
114   // `renderThreadHandler` is a handler for communicating with `renderThread`, and is synchronized
115   // on `handlerLock`.
116   private final Object handlerLock = new Object();
117   @Nullable private Handler renderThreadHandler;
118 
119   private final ArrayList<FrameListenerAndParams> frameListeners = new ArrayList<>();
120 
121   private volatile ErrorCallback errorCallback;
122 
123   // Variables for fps reduction.
124   private final Object fpsReductionLock = new Object();
125   // Time for when next frame should be rendered.
126   private long nextFrameTimeNs;
127   // Minimum duration between frames when fps reduction is active, or -1 if video is completely
128   // paused.
129   private long minRenderPeriodNs;
130 
131   // EGL and GL resources for drawing YUV/OES textures. After initialization, these are only
132   // accessed from the render thread.
133   @Nullable private EglBase eglBase;
134   private final VideoFrameDrawer frameDrawer;
135   @Nullable private RendererCommon.GlDrawer drawer;
136   private boolean usePresentationTimeStamp;
137   private final Matrix drawMatrix = new Matrix();
138 
139   // Pending frame to render. Serves as a queue with size 1. Synchronized on `frameLock`.
140   private final Object frameLock = new Object();
141   @Nullable private VideoFrame pendingFrame;
142 
143   // These variables are synchronized on `layoutLock`.
144   private final Object layoutLock = new Object();
145   private float layoutAspectRatio;
146   // If true, mirrors the video stream horizontally.
147   private boolean mirrorHorizontally;
148   // If true, mirrors the video stream vertically.
149   private boolean mirrorVertically;
150 
151   // These variables are synchronized on `statisticsLock`.
152   private final Object statisticsLock = new Object();
153   // Total number of video frames received in renderFrame() call.
154   private int framesReceived;
155   // Number of video frames dropped by renderFrame() because previous frame has not been rendered
156   // yet.
157   private int framesDropped;
158   // Number of rendered video frames.
159   private int framesRendered;
160   // Start time for counting these statistics, or 0 if we haven't started measuring yet.
161   private long statisticsStartTimeNs;
162   // Time in ns spent in renderFrameOnRenderThread() function.
163   private long renderTimeNs;
164   // Time in ns spent by the render thread in the swapBuffers() function.
165   private long renderSwapBufferTimeNs;
166 
167   // Used for bitmap capturing.
168   private final GlTextureFrameBuffer bitmapTextureFramebuffer =
169       new GlTextureFrameBuffer(GLES20.GL_RGBA);
170 
171   private final Runnable logStatisticsRunnable = new Runnable() {
172     @Override
173     public void run() {
174       logStatistics();
175       synchronized (handlerLock) {
176         if (renderThreadHandler != null) {
177           renderThreadHandler.removeCallbacks(logStatisticsRunnable);
178           renderThreadHandler.postDelayed(
179               logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC));
180         }
181       }
182     }
183   };
184 
185   private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation();
186 
187   /**
188    * Standard constructor. The name will be used for the render thread name and included when
189    * logging. In order to render something, you must first call init() and createEglSurface.
190    */
EglRenderer(String name)191   public EglRenderer(String name) {
192     this(name, new VideoFrameDrawer());
193   }
194 
EglRenderer(String name, VideoFrameDrawer videoFrameDrawer)195   public EglRenderer(String name, VideoFrameDrawer videoFrameDrawer) {
196     this.name = name;
197     this.frameDrawer = videoFrameDrawer;
198   }
199 
200   /**
201    * Initialize this class, sharing resources with `sharedContext`. The custom `drawer` will be used
202    * for drawing frames on the EGLSurface. This class is responsible for calling release() on
203    * `drawer`. It is allowed to call init() to reinitialize the renderer after a previous
204    * init()/release() cycle. If usePresentationTimeStamp is true, eglPresentationTimeANDROID will be
205    * set with the frame timestamps, which specifies desired presentation time and might be useful
206    * for e.g. syncing audio and video.
207    */
init(@ullable final EglBase.Context sharedContext, final int[] configAttributes, RendererCommon.GlDrawer drawer, boolean usePresentationTimeStamp)208   public void init(@Nullable final EglBase.Context sharedContext, final int[] configAttributes,
209       RendererCommon.GlDrawer drawer, boolean usePresentationTimeStamp) {
210     synchronized (handlerLock) {
211       if (renderThreadHandler != null) {
212         throw new IllegalStateException(name + "Already initialized");
213       }
214       logD("Initializing EglRenderer");
215       this.drawer = drawer;
216       this.usePresentationTimeStamp = usePresentationTimeStamp;
217 
218       final HandlerThread renderThread = new HandlerThread(name + "EglRenderer");
219       renderThread.start();
220       renderThreadHandler =
221           new HandlerWithExceptionCallback(renderThread.getLooper(), new Runnable() {
222             @Override
223             public void run() {
224               synchronized (handlerLock) {
225                 renderThreadHandler = null;
226               }
227             }
228           });
229       // Create EGL context on the newly created render thread. It should be possibly to create the
230       // context on this thread and make it current on the render thread, but this causes failure on
231       // some Marvel based JB devices. https://bugs.chromium.org/p/webrtc/issues/detail?id=6350.
232       ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, () -> {
233         // If sharedContext is null, then texture frames are disabled. This is typically for old
234         // devices that might not be fully spec compliant, so force EGL 1.0 since EGL 1.4 has
235         // caused trouble on some weird devices.
236         if (sharedContext == null) {
237           logD("EglBase10.create context");
238           eglBase = EglBase.createEgl10(configAttributes);
239         } else {
240           logD("EglBase.create shared context");
241           eglBase = EglBase.create(sharedContext, configAttributes);
242         }
243       });
244       renderThreadHandler.post(eglSurfaceCreationRunnable);
245       final long currentTimeNs = System.nanoTime();
246       resetStatistics(currentTimeNs);
247       renderThreadHandler.postDelayed(
248           logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC));
249     }
250   }
251 
252   /**
253    * Same as above with usePresentationTimeStamp set to false.
254    *
255    * @see #init(EglBase.Context, int[], RendererCommon.GlDrawer, boolean)
256    */
init(@ullable final EglBase.Context sharedContext, final int[] configAttributes, RendererCommon.GlDrawer drawer)257   public void init(@Nullable final EglBase.Context sharedContext, final int[] configAttributes,
258       RendererCommon.GlDrawer drawer) {
259     init(sharedContext, configAttributes, drawer, /* usePresentationTimeStamp= */ false);
260   }
261 
createEglSurface(Surface surface)262   public void createEglSurface(Surface surface) {
263     createEglSurfaceInternal(surface);
264   }
265 
createEglSurface(SurfaceTexture surfaceTexture)266   public void createEglSurface(SurfaceTexture surfaceTexture) {
267     createEglSurfaceInternal(surfaceTexture);
268   }
269 
createEglSurfaceInternal(Object surface)270   private void createEglSurfaceInternal(Object surface) {
271     eglSurfaceCreationRunnable.setSurface(surface);
272     postToRenderThread(eglSurfaceCreationRunnable);
273   }
274 
275   /**
276    * Block until any pending frame is returned and all GL resources released, even if an interrupt
277    * occurs. If an interrupt occurs during release(), the interrupt flag will be set. This function
278    * should be called before the Activity is destroyed and the EGLContext is still valid. If you
279    * don't call this function, the GL resources might leak.
280    */
release()281   public void release() {
282     logD("Releasing.");
283     final CountDownLatch eglCleanupBarrier = new CountDownLatch(1);
284     synchronized (handlerLock) {
285       if (renderThreadHandler == null) {
286         logD("Already released");
287         return;
288       }
289       renderThreadHandler.removeCallbacks(logStatisticsRunnable);
290       // Release EGL and GL resources on render thread.
291       renderThreadHandler.postAtFrontOfQueue(() -> {
292         // Detach current shader program.
293         synchronized (EglBase.lock) {
294           GLES20.glUseProgram(/* program= */ 0);
295         }
296         if (drawer != null) {
297           drawer.release();
298           drawer = null;
299         }
300         frameDrawer.release();
301         bitmapTextureFramebuffer.release();
302         if (eglBase != null) {
303           logD("eglBase detach and release.");
304           eglBase.detachCurrent();
305           eglBase.release();
306           eglBase = null;
307         }
308         frameListeners.clear();
309         eglCleanupBarrier.countDown();
310       });
311       final Looper renderLooper = renderThreadHandler.getLooper();
312       // TODO(magjed): Replace this post() with renderLooper.quitSafely() when API support >= 18.
313       renderThreadHandler.post(() -> {
314         logD("Quitting render thread.");
315         renderLooper.quit();
316       });
317       // Don't accept any more frames or messages to the render thread.
318       renderThreadHandler = null;
319     }
320     // Make sure the EGL/GL cleanup posted above is executed.
321     ThreadUtils.awaitUninterruptibly(eglCleanupBarrier);
322     synchronized (frameLock) {
323       if (pendingFrame != null) {
324         pendingFrame.release();
325         pendingFrame = null;
326       }
327     }
328     logD("Releasing done.");
329   }
330 
331   /**
332    * Reset the statistics logged in logStatistics().
333    */
resetStatistics(long currentTimeNs)334   private void resetStatistics(long currentTimeNs) {
335     synchronized (statisticsLock) {
336       statisticsStartTimeNs = currentTimeNs;
337       framesReceived = 0;
338       framesDropped = 0;
339       framesRendered = 0;
340       renderTimeNs = 0;
341       renderSwapBufferTimeNs = 0;
342     }
343   }
344 
printStackTrace()345   public void printStackTrace() {
346     synchronized (handlerLock) {
347       final Thread renderThread =
348           (renderThreadHandler == null) ? null : renderThreadHandler.getLooper().getThread();
349       if (renderThread != null) {
350         final StackTraceElement[] renderStackTrace = renderThread.getStackTrace();
351         if (renderStackTrace.length > 0) {
352           logW("EglRenderer stack trace:");
353           for (StackTraceElement traceElem : renderStackTrace) {
354             logW(traceElem.toString());
355           }
356         }
357       }
358     }
359   }
360 
361   /**
362    * Set if the video stream should be mirrored horizontally or not.
363    */
setMirror(final boolean mirror)364   public void setMirror(final boolean mirror) {
365     logD("setMirrorHorizontally: " + mirror);
366     synchronized (layoutLock) {
367       this.mirrorHorizontally = mirror;
368     }
369   }
370 
371   /**
372    * Set if the video stream should be mirrored vertically or not.
373    */
setMirrorVertically(final boolean mirrorVertically)374   public void setMirrorVertically(final boolean mirrorVertically) {
375     logD("setMirrorVertically: " + mirrorVertically);
376     synchronized (layoutLock) {
377       this.mirrorVertically = mirrorVertically;
378     }
379   }
380 
381   /**
382    * Set layout aspect ratio. This is used to crop frames when rendering to avoid stretched video.
383    * Set this to 0 to disable cropping.
384    */
setLayoutAspectRatio(float layoutAspectRatio)385   public void setLayoutAspectRatio(float layoutAspectRatio) {
386     logD("setLayoutAspectRatio: " + layoutAspectRatio);
387     synchronized (layoutLock) {
388       this.layoutAspectRatio = layoutAspectRatio;
389     }
390   }
391 
392   /**
393    * Limit render framerate.
394    *
395    * @param fps Limit render framerate to this value, or use Float.POSITIVE_INFINITY to disable fps
396    *            reduction.
397    */
setFpsReduction(float fps)398   public void setFpsReduction(float fps) {
399     logD("setFpsReduction: " + fps);
400     synchronized (fpsReductionLock) {
401       final long previousRenderPeriodNs = minRenderPeriodNs;
402       if (fps <= 0) {
403         minRenderPeriodNs = Long.MAX_VALUE;
404       } else {
405         minRenderPeriodNs = (long) (TimeUnit.SECONDS.toNanos(1) / fps);
406       }
407       if (minRenderPeriodNs != previousRenderPeriodNs) {
408         // Fps reduction changed - reset frame time.
409         nextFrameTimeNs = System.nanoTime();
410       }
411     }
412   }
413 
disableFpsReduction()414   public void disableFpsReduction() {
415     setFpsReduction(Float.POSITIVE_INFINITY /* fps */);
416   }
417 
pauseVideo()418   public void pauseVideo() {
419     setFpsReduction(0 /* fps */);
420   }
421 
422   /**
423    * Register a callback to be invoked when a new video frame has been received. This version uses
424    * the drawer of the EglRenderer that was passed in init.
425    *
426    * @param listener The callback to be invoked. The callback will be invoked on the render thread.
427    *                 It should be lightweight and must not call removeFrameListener.
428    * @param scale    The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
429    *                 required.
430    */
addFrameListener(final FrameListener listener, final float scale)431   public void addFrameListener(final FrameListener listener, final float scale) {
432     addFrameListener(listener, scale, null, false /* applyFpsReduction */);
433   }
434 
435   /**
436    * Register a callback to be invoked when a new video frame has been received.
437    *
438    * @param listener The callback to be invoked. The callback will be invoked on the render thread.
439    *                 It should be lightweight and must not call removeFrameListener.
440    * @param scale    The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
441    *                 required.
442    * @param drawer   Custom drawer to use for this frame listener or null to use the default one.
443    */
addFrameListener( final FrameListener listener, final float scale, final RendererCommon.GlDrawer drawerParam)444   public void addFrameListener(
445       final FrameListener listener, final float scale, final RendererCommon.GlDrawer drawerParam) {
446     addFrameListener(listener, scale, drawerParam, false /* applyFpsReduction */);
447   }
448 
449   /**
450    * Register a callback to be invoked when a new video frame has been received.
451    *
452    * @param listener The callback to be invoked. The callback will be invoked on the render thread.
453    *                 It should be lightweight and must not call removeFrameListener.
454    * @param scale    The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
455    *                 required.
456    * @param drawer   Custom drawer to use for this frame listener or null to use the default one.
457    * @param applyFpsReduction This callback will not be called for frames that have been dropped by
458    *                          FPS reduction.
459    */
addFrameListener(final FrameListener listener, final float scale, @Nullable final RendererCommon.GlDrawer drawerParam, final boolean applyFpsReduction)460   public void addFrameListener(final FrameListener listener, final float scale,
461       @Nullable final RendererCommon.GlDrawer drawerParam, final boolean applyFpsReduction) {
462     postToRenderThread(() -> {
463       final RendererCommon.GlDrawer listenerDrawer = drawerParam == null ? drawer : drawerParam;
464       frameListeners.add(
465           new FrameListenerAndParams(listener, scale, listenerDrawer, applyFpsReduction));
466     });
467   }
468 
469   /**
470    * Remove any pending callback that was added with addFrameListener. If the callback is not in
471    * the queue, nothing happens. It is ensured that callback won't be called after this method
472    * returns.
473    *
474    * @param runnable The callback to remove.
475    */
removeFrameListener(final FrameListener listener)476   public void removeFrameListener(final FrameListener listener) {
477     final CountDownLatch latch = new CountDownLatch(1);
478     synchronized (handlerLock) {
479       if (renderThreadHandler == null) {
480         return;
481       }
482       if (Thread.currentThread() == renderThreadHandler.getLooper().getThread()) {
483         throw new RuntimeException("removeFrameListener must not be called on the render thread.");
484       }
485       postToRenderThread(() -> {
486         latch.countDown();
487         final Iterator<FrameListenerAndParams> iter = frameListeners.iterator();
488         while (iter.hasNext()) {
489           if (iter.next().listener == listener) {
490             iter.remove();
491           }
492         }
493       });
494     }
495     ThreadUtils.awaitUninterruptibly(latch);
496   }
497 
498   /** Can be set in order to be notified about errors encountered during rendering. */
setErrorCallback(ErrorCallback errorCallback)499   public void setErrorCallback(ErrorCallback errorCallback) {
500     this.errorCallback = errorCallback;
501   }
502 
503   // VideoSink interface.
504   @Override
onFrame(VideoFrame frame)505   public void onFrame(VideoFrame frame) {
506     synchronized (statisticsLock) {
507       ++framesReceived;
508     }
509     final boolean dropOldFrame;
510     synchronized (handlerLock) {
511       if (renderThreadHandler == null) {
512         logD("Dropping frame - Not initialized or already released.");
513         return;
514       }
515       synchronized (frameLock) {
516         dropOldFrame = (pendingFrame != null);
517         if (dropOldFrame) {
518           pendingFrame.release();
519         }
520         pendingFrame = frame;
521         pendingFrame.retain();
522         renderThreadHandler.post(this ::renderFrameOnRenderThread);
523       }
524     }
525     if (dropOldFrame) {
526       synchronized (statisticsLock) {
527         ++framesDropped;
528       }
529     }
530   }
531 
532   /**
533    * Release EGL surface. This function will block until the EGL surface is released.
534    */
releaseEglSurface(final Runnable completionCallback)535   public void releaseEglSurface(final Runnable completionCallback) {
536     // Ensure that the render thread is no longer touching the Surface before returning from this
537     // function.
538     eglSurfaceCreationRunnable.setSurface(null /* surface */);
539     synchronized (handlerLock) {
540       if (renderThreadHandler != null) {
541         renderThreadHandler.removeCallbacks(eglSurfaceCreationRunnable);
542         renderThreadHandler.postAtFrontOfQueue(() -> {
543           if (eglBase != null) {
544             eglBase.detachCurrent();
545             eglBase.releaseSurface();
546           }
547           completionCallback.run();
548         });
549         return;
550       }
551     }
552     completionCallback.run();
553   }
554 
555   /**
556    * Private helper function to post tasks safely.
557    */
postToRenderThread(Runnable runnable)558   private void postToRenderThread(Runnable runnable) {
559     synchronized (handlerLock) {
560       if (renderThreadHandler != null) {
561         renderThreadHandler.post(runnable);
562       }
563     }
564   }
565 
clearSurfaceOnRenderThread(float r, float g, float b, float a)566   private void clearSurfaceOnRenderThread(float r, float g, float b, float a) {
567     if (eglBase != null && eglBase.hasSurface()) {
568       logD("clearSurface");
569       GLES20.glClearColor(r, g, b, a);
570       GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
571       eglBase.swapBuffers();
572     }
573   }
574 
575   /**
576    * Post a task to clear the surface to a transparent uniform color.
577    */
clearImage()578   public void clearImage() {
579     clearImage(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */);
580   }
581 
582   /**
583    * Post a task to clear the surface to a specific color.
584    */
clearImage(final float r, final float g, final float b, final float a)585   public void clearImage(final float r, final float g, final float b, final float a) {
586     synchronized (handlerLock) {
587       if (renderThreadHandler == null) {
588         return;
589       }
590       renderThreadHandler.postAtFrontOfQueue(() -> clearSurfaceOnRenderThread(r, g, b, a));
591     }
592   }
593 
594   /**
595    * Renders and releases `pendingFrame`.
596    */
renderFrameOnRenderThread()597   private void renderFrameOnRenderThread() {
598     // Fetch and render `pendingFrame`.
599     final VideoFrame frame;
600     synchronized (frameLock) {
601       if (pendingFrame == null) {
602         return;
603       }
604       frame = pendingFrame;
605       pendingFrame = null;
606     }
607     if (eglBase == null || !eglBase.hasSurface()) {
608       logD("Dropping frame - No surface");
609       frame.release();
610       return;
611     }
612     // Check if fps reduction is active.
613     final boolean shouldRenderFrame;
614     synchronized (fpsReductionLock) {
615       if (minRenderPeriodNs == Long.MAX_VALUE) {
616         // Rendering is paused.
617         shouldRenderFrame = false;
618       } else if (minRenderPeriodNs <= 0) {
619         // FPS reduction is disabled.
620         shouldRenderFrame = true;
621       } else {
622         final long currentTimeNs = System.nanoTime();
623         if (currentTimeNs < nextFrameTimeNs) {
624           logD("Skipping frame rendering - fps reduction is active.");
625           shouldRenderFrame = false;
626         } else {
627           nextFrameTimeNs += minRenderPeriodNs;
628           // The time for the next frame should always be in the future.
629           nextFrameTimeNs = Math.max(nextFrameTimeNs, currentTimeNs);
630           shouldRenderFrame = true;
631         }
632       }
633     }
634 
635     final long startTimeNs = System.nanoTime();
636 
637     final float frameAspectRatio = frame.getRotatedWidth() / (float) frame.getRotatedHeight();
638     final float drawnAspectRatio;
639     synchronized (layoutLock) {
640       drawnAspectRatio = layoutAspectRatio != 0f ? layoutAspectRatio : frameAspectRatio;
641     }
642 
643     final float scaleX;
644     final float scaleY;
645 
646     if (frameAspectRatio > drawnAspectRatio) {
647       scaleX = drawnAspectRatio / frameAspectRatio;
648       scaleY = 1f;
649     } else {
650       scaleX = 1f;
651       scaleY = frameAspectRatio / drawnAspectRatio;
652     }
653 
654     drawMatrix.reset();
655     drawMatrix.preTranslate(0.5f, 0.5f);
656     drawMatrix.preScale(mirrorHorizontally ? -1f : 1f, mirrorVertically ? -1f : 1f);
657     drawMatrix.preScale(scaleX, scaleY);
658     drawMatrix.preTranslate(-0.5f, -0.5f);
659 
660     try {
661       if (shouldRenderFrame) {
662         GLES20.glClearColor(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */);
663         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
664         frameDrawer.drawFrame(frame, drawer, drawMatrix, 0 /* viewportX */, 0 /* viewportY */,
665             eglBase.surfaceWidth(), eglBase.surfaceHeight());
666 
667         final long swapBuffersStartTimeNs = System.nanoTime();
668         if (usePresentationTimeStamp) {
669           eglBase.swapBuffers(frame.getTimestampNs());
670         } else {
671           eglBase.swapBuffers();
672         }
673 
674         final long currentTimeNs = System.nanoTime();
675         synchronized (statisticsLock) {
676           ++framesRendered;
677           renderTimeNs += (currentTimeNs - startTimeNs);
678           renderSwapBufferTimeNs += (currentTimeNs - swapBuffersStartTimeNs);
679         }
680       }
681 
682       notifyCallbacks(frame, shouldRenderFrame);
683     } catch (GlUtil.GlOutOfMemoryException e) {
684       logE("Error while drawing frame", e);
685       final ErrorCallback errorCallback = this.errorCallback;
686       if (errorCallback != null) {
687         errorCallback.onGlOutOfMemory();
688       }
689       // Attempt to free up some resources.
690       drawer.release();
691       frameDrawer.release();
692       bitmapTextureFramebuffer.release();
693       // Continue here on purpose and retry again for next frame. In worst case, this is a continous
694       // problem and no more frames will be drawn.
695     } finally {
696       frame.release();
697     }
698   }
699 
notifyCallbacks(VideoFrame frame, boolean wasRendered)700   private void notifyCallbacks(VideoFrame frame, boolean wasRendered) {
701     if (frameListeners.isEmpty())
702       return;
703 
704     drawMatrix.reset();
705     drawMatrix.preTranslate(0.5f, 0.5f);
706     drawMatrix.preScale(mirrorHorizontally ? -1f : 1f, mirrorVertically ? -1f : 1f);
707     drawMatrix.preScale(1f, -1f); // We want the output to be upside down for Bitmap.
708     drawMatrix.preTranslate(-0.5f, -0.5f);
709 
710     Iterator<FrameListenerAndParams> it = frameListeners.iterator();
711     while (it.hasNext()) {
712       FrameListenerAndParams listenerAndParams = it.next();
713       if (!wasRendered && listenerAndParams.applyFpsReduction) {
714         continue;
715       }
716       it.remove();
717 
718       final int scaledWidth = (int) (listenerAndParams.scale * frame.getRotatedWidth());
719       final int scaledHeight = (int) (listenerAndParams.scale * frame.getRotatedHeight());
720 
721       if (scaledWidth == 0 || scaledHeight == 0) {
722         listenerAndParams.listener.onFrame(null);
723         continue;
724       }
725 
726       bitmapTextureFramebuffer.setSize(scaledWidth, scaledHeight);
727 
728       GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, bitmapTextureFramebuffer.getFrameBufferId());
729       GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
730           GLES20.GL_TEXTURE_2D, bitmapTextureFramebuffer.getTextureId(), 0);
731 
732       GLES20.glClearColor(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */);
733       GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
734       frameDrawer.drawFrame(frame, listenerAndParams.drawer, drawMatrix, 0 /* viewportX */,
735           0 /* viewportY */, scaledWidth, scaledHeight);
736 
737       final ByteBuffer bitmapBuffer = ByteBuffer.allocateDirect(scaledWidth * scaledHeight * 4);
738       GLES20.glViewport(0, 0, scaledWidth, scaledHeight);
739       GLES20.glReadPixels(
740           0, 0, scaledWidth, scaledHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, bitmapBuffer);
741 
742       GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
743       GlUtil.checkNoGLES2Error("EglRenderer.notifyCallbacks");
744 
745       final Bitmap bitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
746       bitmap.copyPixelsFromBuffer(bitmapBuffer);
747       listenerAndParams.listener.onFrame(bitmap);
748     }
749   }
750 
averageTimeAsString(long sumTimeNs, int count)751   private String averageTimeAsString(long sumTimeNs, int count) {
752     return (count <= 0) ? "NA" : TimeUnit.NANOSECONDS.toMicros(sumTimeNs / count) + " us";
753   }
754 
logStatistics()755   private void logStatistics() {
756     final DecimalFormat fpsFormat = new DecimalFormat("#.0");
757     final long currentTimeNs = System.nanoTime();
758     synchronized (statisticsLock) {
759       final long elapsedTimeNs = currentTimeNs - statisticsStartTimeNs;
760       if (elapsedTimeNs <= 0 || (minRenderPeriodNs == Long.MAX_VALUE && framesReceived == 0)) {
761         return;
762       }
763       final float renderFps = framesRendered * TimeUnit.SECONDS.toNanos(1) / (float) elapsedTimeNs;
764       logD("Duration: " + TimeUnit.NANOSECONDS.toMillis(elapsedTimeNs) + " ms."
765           + " Frames received: " + framesReceived + "."
766           + " Dropped: " + framesDropped + "."
767           + " Rendered: " + framesRendered + "."
768           + " Render fps: " + fpsFormat.format(renderFps) + "."
769           + " Average render time: " + averageTimeAsString(renderTimeNs, framesRendered) + "."
770           + " Average swapBuffer time: "
771           + averageTimeAsString(renderSwapBufferTimeNs, framesRendered) + ".");
772       resetStatistics(currentTimeNs);
773     }
774   }
775 
logE(String string, Throwable e)776   private void logE(String string, Throwable e) {
777     Logging.e(TAG, name + string, e);
778   }
779 
logD(String string)780   private void logD(String string) {
781     Logging.d(TAG, name + string);
782   }
783 
logW(String string)784   private void logW(String string) {
785     Logging.w(TAG, name + string);
786   }
787 }
788