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