| OLD | NEW |
| (Empty) |
| 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.content.Context; | |
| 14 import android.content.res.Resources.NotFoundException; | |
| 15 import android.graphics.Point; | |
| 16 import android.opengl.GLES20; | |
| 17 import android.os.Handler; | |
| 18 import android.os.HandlerThread; | |
| 19 import android.util.AttributeSet; | |
| 20 import android.view.SurfaceHolder; | |
| 21 import android.view.SurfaceView; | |
| 22 | |
| 23 import org.webrtc.Logging; | |
| 24 | |
| 25 import java.util.concurrent.CountDownLatch; | |
| 26 | |
| 27 import javax.microedition.khronos.egl.EGLContext; | |
| 28 | |
| 29 /** | |
| 30 * Implements org.webrtc.VideoRenderer.Callbacks by displaying the video stream
on a SurfaceView. | |
| 31 * renderFrame() is asynchronous to avoid blocking the calling thread. | |
| 32 * This class is thread safe and handles access from potentially four different
threads: | |
| 33 * Interaction from the main app in init, release, setMirror, and setScalingtype
. | |
| 34 * Interaction from C++ rtc::VideoSinkInterface in renderFrame. | |
| 35 * Interaction from the Activity lifecycle in surfaceCreated, surfaceChanged, an
d surfaceDestroyed. | |
| 36 * Interaction with the layout framework in onMeasure and onSizeChanged. | |
| 37 */ | |
| 38 public class SurfaceViewRenderer extends SurfaceView | |
| 39 implements SurfaceHolder.Callback, VideoRenderer.Callbacks { | |
| 40 private static final String TAG = "SurfaceViewRenderer"; | |
| 41 | |
| 42 // Dedicated render thread. | |
| 43 private HandlerThread renderThread; | |
| 44 // |renderThreadHandler| is a handler for communicating with |renderThread|, a
nd is synchronized | |
| 45 // on |handlerLock|. | |
| 46 private final Object handlerLock = new Object(); | |
| 47 private Handler renderThreadHandler; | |
| 48 | |
| 49 // EGL and GL resources for drawing YUV/OES textures. After initilization, the
se are only accessed | |
| 50 // from the render thread. | |
| 51 private EglBase eglBase; | |
| 52 private final RendererCommon.YuvUploader yuvUploader = new RendererCommon.YuvU
ploader(); | |
| 53 private RendererCommon.GlDrawer drawer; | |
| 54 // Texture ids for YUV frames. Allocated on first arrival of a YUV frame. | |
| 55 private int[] yuvTextures = null; | |
| 56 | |
| 57 // Pending frame to render. Serves as a queue with size 1. Synchronized on |fr
ameLock|. | |
| 58 private final Object frameLock = new Object(); | |
| 59 private VideoRenderer.I420Frame pendingFrame; | |
| 60 | |
| 61 // These variables are synchronized on |layoutLock|. | |
| 62 private final Object layoutLock = new Object(); | |
| 63 // These dimension values are used to keep track of the state in these functio
ns: onMeasure(), | |
| 64 // onLayout(), and surfaceChanged(). A new layout is triggered with requestLay
out(). This happens | |
| 65 // internally when the incoming frame size changes. requestLayout() can also b
e triggered | |
| 66 // externally. The layout change is a two pass process: first onMeasure() is c
alled in a top-down | |
| 67 // traversal of the View tree, followed by an onLayout() pass that is also top
-down. During the | |
| 68 // onLayout() pass, each parent is responsible for positioning its children us
ing the sizes | |
| 69 // computed in the measure pass. | |
| 70 // |desiredLayoutsize| is the layout size we have requested in onMeasure() and
are waiting for to | |
| 71 // take effect. | |
| 72 private Point desiredLayoutSize = new Point(); | |
| 73 // |layoutSize|/|surfaceSize| is the actual current layout/surface size. They
are updated in | |
| 74 // onLayout() and surfaceChanged() respectively. | |
| 75 private final Point layoutSize = new Point(); | |
| 76 // TODO(magjed): Enable hardware scaler with SurfaceHolder.setFixedSize(). Thi
s will decouple | |
| 77 // layout and surface size. | |
| 78 private final Point surfaceSize = new Point(); | |
| 79 // |isSurfaceCreated| keeps track of the current status in surfaceCreated()/su
rfaceDestroyed(). | |
| 80 private boolean isSurfaceCreated; | |
| 81 // Last rendered frame dimensions, or 0 if no frame has been rendered yet. | |
| 82 private int frameWidth; | |
| 83 private int frameHeight; | |
| 84 private int frameRotation; | |
| 85 // |scalingType| determines how the video will fill the allowed layout area in
onMeasure(). | |
| 86 private RendererCommon.ScalingType scalingType = RendererCommon.ScalingType.SC
ALE_ASPECT_BALANCED; | |
| 87 // If true, mirrors the video stream horizontally. | |
| 88 private boolean mirror; | |
| 89 // Callback for reporting renderer events. | |
| 90 private RendererCommon.RendererEvents rendererEvents; | |
| 91 | |
| 92 // These variables are synchronized on |statisticsLock|. | |
| 93 private final Object statisticsLock = new Object(); | |
| 94 // Total number of video frames received in renderFrame() call. | |
| 95 private int framesReceived; | |
| 96 // Number of video frames dropped by renderFrame() because previous frame has
not been rendered | |
| 97 // yet. | |
| 98 private int framesDropped; | |
| 99 // Number of rendered video frames. | |
| 100 private int framesRendered; | |
| 101 // Time in ns when the first video frame was rendered. | |
| 102 private long firstFrameTimeNs; | |
| 103 // Time in ns spent in renderFrameOnRenderThread() function. | |
| 104 private long renderTimeNs; | |
| 105 | |
| 106 // Runnable for posting frames to render thread. | |
| 107 private final Runnable renderFrameRunnable = new Runnable() { | |
| 108 @Override public void run() { | |
| 109 renderFrameOnRenderThread(); | |
| 110 } | |
| 111 }; | |
| 112 // Runnable for clearing Surface to black. | |
| 113 private final Runnable makeBlackRunnable = new Runnable() { | |
| 114 @Override public void run() { | |
| 115 makeBlack(); | |
| 116 } | |
| 117 }; | |
| 118 | |
| 119 /** | |
| 120 * Standard View constructor. In order to render something, you must first cal
l init(). | |
| 121 */ | |
| 122 public SurfaceViewRenderer(Context context) { | |
| 123 super(context); | |
| 124 getHolder().addCallback(this); | |
| 125 } | |
| 126 | |
| 127 /** | |
| 128 * Standard View constructor. In order to render something, you must first cal
l init(). | |
| 129 */ | |
| 130 public SurfaceViewRenderer(Context context, AttributeSet attrs) { | |
| 131 super(context, attrs); | |
| 132 getHolder().addCallback(this); | |
| 133 } | |
| 134 | |
| 135 /** | |
| 136 * Initialize this class, sharing resources with |sharedContext|. It is allowe
d to call init() to | |
| 137 * reinitialize the renderer after a previous init()/release() cycle. | |
| 138 */ | |
| 139 public void init( | |
| 140 EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvent
s) { | |
| 141 init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer()
); | |
| 142 } | |
| 143 | |
| 144 /** | |
| 145 * Initialize this class, sharing resources with |sharedContext|. The custom |
drawer| will be used | |
| 146 * for drawing frames on the EGLSurface. This class is responsible for calling
release() on | |
| 147 * |drawer|. It is allowed to call init() to reinitialize the renderer after a
previous | |
| 148 * init()/release() cycle. | |
| 149 */ | |
| 150 public void init(EglBase.Context sharedContext, RendererCommon.RendererEvents
rendererEvents, | |
| 151 int[] configAttributes, RendererCommon.GlDrawer drawer) { | |
| 152 synchronized (handlerLock) { | |
| 153 if (renderThreadHandler != null) { | |
| 154 throw new IllegalStateException(getResourceName() + "Already initialized
"); | |
| 155 } | |
| 156 Logging.d(TAG, getResourceName() + "Initializing."); | |
| 157 this.rendererEvents = rendererEvents; | |
| 158 this.drawer = drawer; | |
| 159 renderThread = new HandlerThread(TAG); | |
| 160 renderThread.start(); | |
| 161 eglBase = EglBase.create(sharedContext, configAttributes); | |
| 162 renderThreadHandler = new Handler(renderThread.getLooper()); | |
| 163 } | |
| 164 tryCreateEglSurface(); | |
| 165 } | |
| 166 | |
| 167 /** | |
| 168 * Create and make an EGLSurface current if both init() and surfaceCreated() h
ave been called. | |
| 169 */ | |
| 170 public void tryCreateEglSurface() { | |
| 171 // |renderThreadHandler| is only created after |eglBase| is created in init(
), so the | |
| 172 // following code will only execute if eglBase != null. | |
| 173 runOnRenderThread(new Runnable() { | |
| 174 @Override public void run() { | |
| 175 synchronized (layoutLock) { | |
| 176 if (isSurfaceCreated && !eglBase.hasSurface()) { | |
| 177 eglBase.createSurface(getHolder().getSurface()); | |
| 178 eglBase.makeCurrent(); | |
| 179 // Necessary for YUV frames with odd width. | |
| 180 GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); | |
| 181 } | |
| 182 } | |
| 183 } | |
| 184 }); | |
| 185 } | |
| 186 | |
| 187 /** | |
| 188 * Block until any pending frame is returned and all GL resources released, ev
en if an interrupt | |
| 189 * occurs. If an interrupt occurs during release(), the interrupt flag will be
set. This function | |
| 190 * should be called before the Activity is destroyed and the EGLContext is sti
ll valid. If you | |
| 191 * don't call this function, the GL resources might leak. | |
| 192 */ | |
| 193 public void release() { | |
| 194 final CountDownLatch eglCleanupBarrier = new CountDownLatch(1); | |
| 195 synchronized (handlerLock) { | |
| 196 if (renderThreadHandler == null) { | |
| 197 Logging.d(TAG, getResourceName() + "Already released"); | |
| 198 return; | |
| 199 } | |
| 200 // Release EGL and GL resources on render thread. | |
| 201 // TODO(magjed): This might not be necessary - all OpenGL resources are au
tomatically deleted | |
| 202 // when the EGL context is lost. It might be dangerous to delete them manu
ally in | |
| 203 // Activity.onDestroy(). | |
| 204 renderThreadHandler.postAtFrontOfQueue(new Runnable() { | |
| 205 @Override public void run() { | |
| 206 drawer.release(); | |
| 207 drawer = null; | |
| 208 if (yuvTextures != null) { | |
| 209 GLES20.glDeleteTextures(3, yuvTextures, 0); | |
| 210 yuvTextures = null; | |
| 211 } | |
| 212 // Clear last rendered image to black. | |
| 213 makeBlack(); | |
| 214 eglBase.release(); | |
| 215 eglBase = null; | |
| 216 eglCleanupBarrier.countDown(); | |
| 217 } | |
| 218 }); | |
| 219 // Don't accept any more frames or messages to the render thread. | |
| 220 renderThreadHandler = null; | |
| 221 } | |
| 222 // Make sure the EGL/GL cleanup posted above is executed. | |
| 223 ThreadUtils.awaitUninterruptibly(eglCleanupBarrier); | |
| 224 renderThread.quit(); | |
| 225 synchronized (frameLock) { | |
| 226 if (pendingFrame != null) { | |
| 227 VideoRenderer.renderFrameDone(pendingFrame); | |
| 228 pendingFrame = null; | |
| 229 } | |
| 230 } | |
| 231 // The |renderThread| cleanup is not safe to cancel and we need to wait unti
l it's done. | |
| 232 ThreadUtils.joinUninterruptibly(renderThread); | |
| 233 renderThread = null; | |
| 234 // Reset statistics and event reporting. | |
| 235 synchronized (layoutLock) { | |
| 236 frameWidth = 0; | |
| 237 frameHeight = 0; | |
| 238 frameRotation = 0; | |
| 239 rendererEvents = null; | |
| 240 } | |
| 241 resetStatistics(); | |
| 242 } | |
| 243 | |
| 244 /** | |
| 245 * Reset statistics. This will reset the logged statistics in logStatistics(),
and | |
| 246 * RendererEvents.onFirstFrameRendered() will be called for the next frame. | |
| 247 */ | |
| 248 public void resetStatistics() { | |
| 249 synchronized (statisticsLock) { | |
| 250 framesReceived = 0; | |
| 251 framesDropped = 0; | |
| 252 framesRendered = 0; | |
| 253 firstFrameTimeNs = 0; | |
| 254 renderTimeNs = 0; | |
| 255 } | |
| 256 } | |
| 257 | |
| 258 /** | |
| 259 * Set if the video stream should be mirrored or not. | |
| 260 */ | |
| 261 public void setMirror(final boolean mirror) { | |
| 262 synchronized (layoutLock) { | |
| 263 this.mirror = mirror; | |
| 264 } | |
| 265 } | |
| 266 | |
| 267 /** | |
| 268 * Set how the video will fill the allowed layout area. | |
| 269 */ | |
| 270 public void setScalingType(RendererCommon.ScalingType scalingType) { | |
| 271 synchronized (layoutLock) { | |
| 272 this.scalingType = scalingType; | |
| 273 } | |
| 274 } | |
| 275 | |
| 276 // VideoRenderer.Callbacks interface. | |
| 277 @Override | |
| 278 public void renderFrame(VideoRenderer.I420Frame frame) { | |
| 279 synchronized (statisticsLock) { | |
| 280 ++framesReceived; | |
| 281 } | |
| 282 synchronized (handlerLock) { | |
| 283 if (renderThreadHandler == null) { | |
| 284 Logging.d(TAG, getResourceName() | |
| 285 + "Dropping frame - Not initialized or already released."); | |
| 286 VideoRenderer.renderFrameDone(frame); | |
| 287 return; | |
| 288 } | |
| 289 synchronized (frameLock) { | |
| 290 if (pendingFrame != null) { | |
| 291 // Drop old frame. | |
| 292 synchronized (statisticsLock) { | |
| 293 ++framesDropped; | |
| 294 } | |
| 295 VideoRenderer.renderFrameDone(pendingFrame); | |
| 296 } | |
| 297 pendingFrame = frame; | |
| 298 updateFrameDimensionsAndReportEvents(frame); | |
| 299 renderThreadHandler.post(renderFrameRunnable); | |
| 300 } | |
| 301 } | |
| 302 } | |
| 303 | |
| 304 // Returns desired layout size given current measure specification and video a
spect ratio. | |
| 305 private Point getDesiredLayoutSize(int widthSpec, int heightSpec) { | |
| 306 synchronized (layoutLock) { | |
| 307 final int maxWidth = getDefaultSize(Integer.MAX_VALUE, widthSpec); | |
| 308 final int maxHeight = getDefaultSize(Integer.MAX_VALUE, heightSpec); | |
| 309 final Point size = | |
| 310 RendererCommon.getDisplaySize(scalingType, frameAspectRatio(), maxWidt
h, maxHeight); | |
| 311 if (MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY) { | |
| 312 size.x = maxWidth; | |
| 313 } | |
| 314 if (MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY) { | |
| 315 size.y = maxHeight; | |
| 316 } | |
| 317 return size; | |
| 318 } | |
| 319 } | |
| 320 | |
| 321 // View layout interface. | |
| 322 @Override | |
| 323 protected void onMeasure(int widthSpec, int heightSpec) { | |
| 324 synchronized (layoutLock) { | |
| 325 if (frameWidth == 0 || frameHeight == 0) { | |
| 326 super.onMeasure(widthSpec, heightSpec); | |
| 327 return; | |
| 328 } | |
| 329 desiredLayoutSize = getDesiredLayoutSize(widthSpec, heightSpec); | |
| 330 if (desiredLayoutSize.x != getMeasuredWidth() || desiredLayoutSize.y != ge
tMeasuredHeight()) { | |
| 331 // Clear the surface asap before the layout change to avoid stretched vi
deo and other | |
| 332 // render artifacs. Don't wait for it to finish because the IO thread sh
ould never be | |
| 333 // blocked, so it's a best-effort attempt. | |
| 334 synchronized (handlerLock) { | |
| 335 if (renderThreadHandler != null) { | |
| 336 renderThreadHandler.postAtFrontOfQueue(makeBlackRunnable); | |
| 337 } | |
| 338 } | |
| 339 } | |
| 340 setMeasuredDimension(desiredLayoutSize.x, desiredLayoutSize.y); | |
| 341 } | |
| 342 } | |
| 343 | |
| 344 @Override | |
| 345 protected void onLayout(boolean changed, int left, int top, int right, int bot
tom) { | |
| 346 synchronized (layoutLock) { | |
| 347 layoutSize.x = right - left; | |
| 348 layoutSize.y = bottom - top; | |
| 349 } | |
| 350 // Might have a pending frame waiting for a layout of correct size. | |
| 351 runOnRenderThread(renderFrameRunnable); | |
| 352 } | |
| 353 | |
| 354 // SurfaceHolder.Callback interface. | |
| 355 @Override | |
| 356 public void surfaceCreated(final SurfaceHolder holder) { | |
| 357 Logging.d(TAG, getResourceName() + "Surface created."); | |
| 358 synchronized (layoutLock) { | |
| 359 isSurfaceCreated = true; | |
| 360 } | |
| 361 tryCreateEglSurface(); | |
| 362 } | |
| 363 | |
| 364 @Override | |
| 365 public void surfaceDestroyed(SurfaceHolder holder) { | |
| 366 Logging.d(TAG, getResourceName() + "Surface destroyed."); | |
| 367 synchronized (layoutLock) { | |
| 368 isSurfaceCreated = false; | |
| 369 surfaceSize.x = 0; | |
| 370 surfaceSize.y = 0; | |
| 371 } | |
| 372 runOnRenderThread(new Runnable() { | |
| 373 @Override public void run() { | |
| 374 eglBase.releaseSurface(); | |
| 375 } | |
| 376 }); | |
| 377 } | |
| 378 | |
| 379 @Override | |
| 380 public void surfaceChanged(SurfaceHolder holder, int format, int width, int he
ight) { | |
| 381 Logging.d(TAG, getResourceName() + "Surface changed: " + width + "x" + heigh
t); | |
| 382 synchronized (layoutLock) { | |
| 383 surfaceSize.x = width; | |
| 384 surfaceSize.y = height; | |
| 385 } | |
| 386 // Might have a pending frame waiting for a surface of correct size. | |
| 387 runOnRenderThread(renderFrameRunnable); | |
| 388 } | |
| 389 | |
| 390 /** | |
| 391 * Private helper function to post tasks safely. | |
| 392 */ | |
| 393 private void runOnRenderThread(Runnable runnable) { | |
| 394 synchronized (handlerLock) { | |
| 395 if (renderThreadHandler != null) { | |
| 396 renderThreadHandler.post(runnable); | |
| 397 } | |
| 398 } | |
| 399 } | |
| 400 | |
| 401 private String getResourceName() { | |
| 402 try { | |
| 403 return getResources().getResourceEntryName(getId()) + ": "; | |
| 404 } catch (NotFoundException e) { | |
| 405 return ""; | |
| 406 } | |
| 407 } | |
| 408 | |
| 409 private void makeBlack() { | |
| 410 if (Thread.currentThread() != renderThread) { | |
| 411 throw new IllegalStateException(getResourceName() + "Wrong thread."); | |
| 412 } | |
| 413 if (eglBase != null && eglBase.hasSurface()) { | |
| 414 GLES20.glClearColor(0, 0, 0, 0); | |
| 415 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); | |
| 416 eglBase.swapBuffers(); | |
| 417 } | |
| 418 } | |
| 419 | |
| 420 /** | |
| 421 * Requests new layout if necessary. Returns true if layout and surface size a
re consistent. | |
| 422 */ | |
| 423 private boolean checkConsistentLayout() { | |
| 424 if (Thread.currentThread() != renderThread) { | |
| 425 throw new IllegalStateException(getResourceName() + "Wrong thread."); | |
| 426 } | |
| 427 synchronized (layoutLock) { | |
| 428 // Return false while we are in the middle of a layout change. | |
| 429 return layoutSize.equals(desiredLayoutSize) && surfaceSize.equals(layoutSi
ze); | |
| 430 } | |
| 431 } | |
| 432 | |
| 433 /** | |
| 434 * Renders and releases |pendingFrame|. | |
| 435 */ | |
| 436 private void renderFrameOnRenderThread() { | |
| 437 if (Thread.currentThread() != renderThread) { | |
| 438 throw new IllegalStateException(getResourceName() + "Wrong thread."); | |
| 439 } | |
| 440 // Fetch and render |pendingFrame|. | |
| 441 final VideoRenderer.I420Frame frame; | |
| 442 synchronized (frameLock) { | |
| 443 if (pendingFrame == null) { | |
| 444 return; | |
| 445 } | |
| 446 frame = pendingFrame; | |
| 447 pendingFrame = null; | |
| 448 } | |
| 449 if (eglBase == null || !eglBase.hasSurface()) { | |
| 450 Logging.d(TAG, getResourceName() + "No surface to draw on"); | |
| 451 VideoRenderer.renderFrameDone(frame); | |
| 452 return; | |
| 453 } | |
| 454 if (!checkConsistentLayout()) { | |
| 455 // Output intermediate black frames while the layout is updated. | |
| 456 makeBlack(); | |
| 457 VideoRenderer.renderFrameDone(frame); | |
| 458 return; | |
| 459 } | |
| 460 // After a surface size change, the EGLSurface might still have a buffer of
the old size in the | |
| 461 // pipeline. Querying the EGLSurface will show if the underlying buffer dime
nsions haven't yet | |
| 462 // changed. Such a buffer will be rendered incorrectly, so flush it with a b
lack frame. | |
| 463 synchronized (layoutLock) { | |
| 464 if (eglBase.surfaceWidth() != surfaceSize.x || eglBase.surfaceHeight() !=
surfaceSize.y) { | |
| 465 makeBlack(); | |
| 466 } | |
| 467 } | |
| 468 | |
| 469 final long startTimeNs = System.nanoTime(); | |
| 470 final float[] texMatrix; | |
| 471 synchronized (layoutLock) { | |
| 472 final float[] rotatedSamplingMatrix = | |
| 473 RendererCommon.rotateTextureMatrix(frame.samplingMatrix, frame.rotatio
nDegree); | |
| 474 final float[] layoutMatrix = RendererCommon.getLayoutMatrix( | |
| 475 mirror, frameAspectRatio(), (float) layoutSize.x / layoutSize.y); | |
| 476 texMatrix = RendererCommon.multiplyMatrices(rotatedSamplingMatrix, layoutM
atrix); | |
| 477 } | |
| 478 | |
| 479 // TODO(magjed): glClear() shouldn't be necessary since every pixel is cover
ed anyway, but it's | |
| 480 // a workaround for bug 5147. Performance will be slightly worse. | |
| 481 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); | |
| 482 if (frame.yuvFrame) { | |
| 483 // Make sure YUV textures are allocated. | |
| 484 if (yuvTextures == null) { | |
| 485 yuvTextures = new int[3]; | |
| 486 for (int i = 0; i < 3; i++) { | |
| 487 yuvTextures[i] = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D); | |
| 488 } | |
| 489 } | |
| 490 yuvUploader.uploadYuvData( | |
| 491 yuvTextures, frame.width, frame.height, frame.yuvStrides, frame.yuvPla
nes); | |
| 492 drawer.drawYuv(yuvTextures, texMatrix, frame.rotatedWidth(), frame.rotated
Height(), | |
| 493 0, 0, surfaceSize.x, surfaceSize.y); | |
| 494 } else { | |
| 495 drawer.drawOes(frame.textureId, texMatrix, frame.rotatedWidth(), frame.rot
atedHeight(), | |
| 496 0, 0, surfaceSize.x, surfaceSize.y); | |
| 497 } | |
| 498 | |
| 499 eglBase.swapBuffers(); | |
| 500 VideoRenderer.renderFrameDone(frame); | |
| 501 synchronized (statisticsLock) { | |
| 502 if (framesRendered == 0) { | |
| 503 firstFrameTimeNs = startTimeNs; | |
| 504 synchronized (layoutLock) { | |
| 505 Logging.d(TAG, getResourceName() + "Reporting first rendered frame."); | |
| 506 if (rendererEvents != null) { | |
| 507 rendererEvents.onFirstFrameRendered(); | |
| 508 } | |
| 509 } | |
| 510 } | |
| 511 ++framesRendered; | |
| 512 renderTimeNs += (System.nanoTime() - startTimeNs); | |
| 513 if (framesRendered % 300 == 0) { | |
| 514 logStatistics(); | |
| 515 } | |
| 516 } | |
| 517 } | |
| 518 | |
| 519 // Return current frame aspect ratio, taking rotation into account. | |
| 520 private float frameAspectRatio() { | |
| 521 synchronized (layoutLock) { | |
| 522 if (frameWidth == 0 || frameHeight == 0) { | |
| 523 return 0.0f; | |
| 524 } | |
| 525 return (frameRotation % 180 == 0) ? (float) frameWidth / frameHeight | |
| 526 : (float) frameHeight / frameWidth; | |
| 527 } | |
| 528 } | |
| 529 | |
| 530 // Update frame dimensions and report any changes to |rendererEvents|. | |
| 531 private void updateFrameDimensionsAndReportEvents(VideoRenderer.I420Frame fram
e) { | |
| 532 synchronized (layoutLock) { | |
| 533 if (frameWidth != frame.width || frameHeight != frame.height | |
| 534 || frameRotation != frame.rotationDegree) { | |
| 535 Logging.d(TAG, getResourceName() + "Reporting frame resolution changed t
o " | |
| 536 + frame.width + "x" + frame.height + " with rotation " + frame.rotat
ionDegree); | |
| 537 if (rendererEvents != null) { | |
| 538 rendererEvents.onFrameResolutionChanged(frame.width, frame.height, fra
me.rotationDegree); | |
| 539 } | |
| 540 frameWidth = frame.width; | |
| 541 frameHeight = frame.height; | |
| 542 frameRotation = frame.rotationDegree; | |
| 543 post(new Runnable() { | |
| 544 @Override public void run() { | |
| 545 requestLayout(); | |
| 546 } | |
| 547 }); | |
| 548 } | |
| 549 } | |
| 550 } | |
| 551 | |
| 552 private void logStatistics() { | |
| 553 synchronized (statisticsLock) { | |
| 554 Logging.d(TAG, getResourceName() + "Frames received: " | |
| 555 + framesReceived + ". Dropped: " + framesDropped + ". Rendered: " + fr
amesRendered); | |
| 556 if (framesReceived > 0 && framesRendered > 0) { | |
| 557 final long timeSinceFirstFrameNs = System.nanoTime() - firstFrameTimeNs; | |
| 558 Logging.d(TAG, getResourceName() + "Duration: " + (int) (timeSinceFirstF
rameNs / 1e6) + | |
| 559 " ms. FPS: " + framesRendered * 1e9 / timeSinceFirstFrameNs); | |
| 560 Logging.d(TAG, getResourceName() + "Average render time: " | |
| 561 + (int) (renderTimeNs / (1000 * framesRendered)) + " us."); | |
| 562 } | |
| 563 } | |
| 564 } | |
| 565 } | |
| OLD | NEW |